UILabel {
13 | let titleLabel = UILabel()
14 | titleLabel.text = title
15 | titleLabel.font = Fonts.heading
16 | titleLabel.textColor = Colors.yellow
17 | let width = titleLabel.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)).width
18 | let tappableHeight: CGFloat = 100.0
19 | titleLabel.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: tappableHeight))
20 | return titleLabel
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ConjugarTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ConjugarTests/Supporting/TestingAppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestingAppDelegate.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 1/31/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | @testable import Conjugar
11 |
12 | @objc(TestingAppDelegate)
13 | final class TestingAppDelegate: UIResponder, UIApplicationDelegate {
14 | var window: UIWindow?
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | Current = World.unitTest
18 | window = UIWindow(frame: UIScreen.main.bounds)
19 | window?.rootViewController = TestingRootViewController()
20 | window?.makeKeyAndVisible()
21 | return true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ConjugarUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Conjugar/TestAnalyticsService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestAnalyticsService.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 11/25/18.
6 | // Copyright © 2018 Josh Adams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | class TestAnalyticsService: AnalyticsServiceable {
12 | private var fire: (String) -> ()
13 |
14 | init(fire: @escaping (String) -> () = { analytic in print(analytic) }) {
15 | self.fire = fire
16 | }
17 |
18 | func recordEvent(_ eventName: String, parameters: [String: String]?, metrics: [String: Double]?) {
19 | var analytic = eventName
20 | if let parameters = parameters {
21 | analytic += " "
22 | for (key, value) in parameters {
23 | analytic += key + ": " + value + " "
24 | }
25 | }
26 | fire(analytic)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Conjugar/IntExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntExtension.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 6/25/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension Int {
12 | var timeString: String {
13 | var remainingSeconds = self
14 | let hours = remainingSeconds / 3600
15 | remainingSeconds -= hours * 3600
16 | let minutes = remainingSeconds / 60
17 | remainingSeconds -= minutes * 60
18 | if hours > 0 {
19 | return NSString(format: "%d:%02d:%02d", hours, minutes, remainingSeconds) as String
20 | } else if minutes > 0 {
21 | return NSString(format: "%d:%02d", minutes, remainingSeconds) as String
22 | } else {
23 | return NSString(format: "%d", remainingSeconds) as String
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Conjugar/Difficulty.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Difficulty.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 6/17/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | enum Difficulty: String, CaseIterable {
10 | case easy = "Easy"
11 | case moderate = "Moderate"
12 | case difficult = "Difficult"
13 |
14 | var scoreModifier: Double {
15 | switch self {
16 | case .easy:
17 | return 0.5
18 | case .moderate:
19 | return 1.0
20 | case .difficult:
21 | return 1.5
22 | }
23 | }
24 |
25 | var localizedDifficulty: String {
26 | switch self {
27 | case .easy:
28 | return Localizations.easy
29 | case .moderate:
30 | return Localizations.moderate
31 | case .difficult:
32 | return Localizations.difficult
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Conjugar/URLProtocolStub.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLProtocolStub.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 4/26/19.
6 | // Based on an article by Paul Hudson.
7 |
8 | import Foundation
9 |
10 | class URLProtocolStub: URLProtocol {
11 | static var testURLs = [URL?: Data]()
12 |
13 | override class func canInit(with request: URLRequest) -> Bool {
14 | return true
15 | }
16 |
17 | override class func canonicalRequest(for request: URLRequest) -> URLRequest {
18 | return request
19 | }
20 |
21 | override func startLoading() {
22 | if let url = request.url {
23 | if let data = URLProtocolStub.testURLs[url] {
24 | self.client?.urlProtocol(self, didLoad: data)
25 | }
26 | }
27 | self.client?.urlProtocolDidFinishLoading(self)
28 | }
29 |
30 | override func stopLoading() { }
31 | }
32 |
--------------------------------------------------------------------------------
/ConjugarTests/Helpers/UIColorExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColorExtension.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/22/19.
6 | // Borrowed from: https://stackoverflow.com/a/48610603/8248798
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIColor {
12 | static func == (l: UIColor, r: UIColor) -> Bool {
13 | var l_red = CGFloat(0); var l_green = CGFloat(0); var l_blue = CGFloat(0); var l_alpha = CGFloat(0)
14 | guard l.getRed(&l_red, green: &l_green, blue: &l_blue, alpha: &l_alpha) else { return false }
15 | var r_red = CGFloat(0); var r_green = CGFloat(0); var r_blue = CGFloat(0); var r_alpha = CGFloat(0)
16 | guard r.getRed(&r_red, green: &r_green, blue: &r_blue, alpha: &r_alpha) else { return false }
17 | return l_red == r_red && l_green == r_green && l_blue == r_blue && l_alpha == r_alpha
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Conjugar/Region.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Region.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 3/31/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | enum Region: String, CaseIterable {
10 | case spain = "Spain"
11 | case latinAmerica = "Latin America"
12 |
13 | var accent: String {
14 | switch self {
15 | case .spain:
16 | return "ES"
17 | case .latinAmerica:
18 | return "MX"
19 | }
20 | }
21 |
22 | var scoreModifier: Double {
23 | switch self {
24 | case .spain:
25 | return 1.0
26 | case .latinAmerica:
27 | return 0.833
28 | }
29 | }
30 |
31 | var localizedRegion: String {
32 | switch self {
33 | case .spain:
34 | return Localizations.spain
35 | case .latinAmerica:
36 | return Localizations.latinAmerica
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Conjugar/Commun.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Commun.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 12/14/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct Commun {
12 | let title: [String: String]
13 | let image: UIImage
14 | let imageLabel: [String: String]
15 | let content: [String: String]
16 | let type: CommunType
17 | let identifier: Int
18 |
19 | enum CommunType {
20 | case information(okayTitle: [String: String])
21 | case newVersion(okayTitle: [String: String], actionTitle: [String: String], cancelTitle: [String: String], action: () -> (), alreadyUpdated: Bool)
22 | case email(actionTitle: [String: String], cancelTitle: [String: String], action: () -> ())
23 | case website(actionTitle: [String: String], cancelTitle: [String: String], action: () -> ())
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Conjugar/TestGameCenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestGameCenter.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 11/27/18.
6 | // Copyright © 2018 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TestGameCenter: GameCenterable {
12 | var isAuthenticated: Bool
13 |
14 | init(isAuthenticated: Bool = false) {
15 | self.isAuthenticated = isAuthenticated
16 | }
17 |
18 | func authenticate(onViewController: UIViewController, completion: ((Bool) -> Void)?) {
19 | if !isAuthenticated {
20 | isAuthenticated = true
21 | completion?(true)
22 | } else {
23 | completion?(false)
24 | }
25 | }
26 |
27 | func reportScore(_ score: Int) {
28 | print("Pretending to report score \(score).")
29 | }
30 |
31 | func showLeaderboard() {
32 | print("Pretending to show leaderboard.")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ConjugarTests/Utils/UIViewExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewExtensionsTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 4/28/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class UIViewExtensionsTests: XCTestCase {
13 | func testPulsate() {
14 | let view = UIView()
15 | view.pulsate()
16 | let expectatiön = expectation(description: "testPulsate")
17 | let duration: TimeInterval = 0.3
18 | let cushion: TimeInterval = 1.0
19 | let timeoutFactor: TimeInterval = 2.0
20 | DispatchQueue.main.asyncAfter(deadline: .now() + duration + cushion, execute: {
21 | XCTAssert(view.transform.isIdentity)
22 | expectatiön.fulfill()
23 | })
24 | wait(for: [expectatiön], timeout: duration + cushion * timeoutFactor)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ConjugarTests/Models/ConjugationResultTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConjugationResultTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/13/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class ConjugationResultTests: XCTestCase {
13 | func testCompare() {
14 | var lhs = ""
15 | var rhs = ""
16 |
17 | XCTAssertEqual(ConjugationResult.compare(lhs: lhs, rhs: rhs), .noMatch)
18 | lhs = " "
19 | XCTAssertEqual(ConjugationResult.compare(lhs: lhs, rhs: rhs), .noMatch)
20 | lhs = "cómo"
21 | XCTAssertEqual(ConjugationResult.compare(lhs: lhs, rhs: rhs), .noMatch)
22 | rhs = "comó"
23 | XCTAssertEqual(ConjugationResult.compare(lhs: lhs, rhs: rhs), .partialMatch)
24 | lhs = "comó"
25 | XCTAssertEqual(ConjugationResult.compare(lhs: lhs, rhs: rhs), .totalMatch)
26 | rhs = "🥥🥥🥥🥥"
27 | XCTAssertEqual(ConjugationResult.compare(lhs: lhs, rhs: rhs), .noMatch)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Conjugar/UIAlertControllerExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIAlertControllerExtension.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 8/19/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIAlertController {
12 | // See http://stackoverflow.com/a/39975404/2084036 for why this needs to be a class method rather than a class property.
13 | static func okTitle() -> String { return Localizations.okay }
14 |
15 | class func showMessage(_ message: String, title: String, okTitle: String, onViewController viewController: UIViewController, handler: ((UIAlertAction) -> Void)? = nil) {
16 | let alertController = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
17 | let okAction = UIAlertAction(title: okTitle, style: UIAlertAction.Style.default, handler: handler)
18 | alertController.addAction(okAction)
19 | viewController.present(alertController, animated: true, completion: nil)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Conjugar/ConjugationResult.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConjugationResult.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 6/20/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | enum ConjugationResult: Int {
10 | case totalMatch = 10
11 | case partialMatch = 5
12 | case noMatch = 0
13 |
14 | static func compare(lhs: String, rhs: String) -> ConjugationResult {
15 | let lhsCount = lhs.count
16 | let rhsCount = rhs.count
17 | if lhsCount != rhsCount || lhsCount == 0 {
18 | return .noMatch
19 | }
20 | var lhsClean = lhs.lowercased()
21 | var rhsClean = rhs.lowercased()
22 | if lhsClean == rhsClean {
23 | return .totalMatch
24 | }
25 | [("á", "a"), ("é", "e"), ("í", "i"), ("ó", "o"), ("ú", "u")].forEach {
26 | lhsClean = lhsClean.replacingOccurrences(of: $0.0, with: $0.1)
27 | rhsClean = rhsClean.replacingOccurrences(of: $0.0, with: $0.1)
28 | }
29 | if lhsClean == rhsClean {
30 | return .partialMatch
31 | } else {
32 | return .noMatch
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Conjugar/Emailer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Emailer.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 12/20/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | import MessageUI
10 |
11 | struct Emailer {
12 | static let shared = Emailer()
13 | private let delegāte = EmailDelegate()
14 |
15 | var sendEmailClosure: (() -> ())? {
16 | if MFMailComposeViewController.canSendMail() {
17 | return {
18 | let mail = MFMailComposeViewController()
19 | mail.mailComposeDelegate = delegāte
20 | mail.setToRecipients(["vermontcoder@gmail.com"])
21 | mail.setSubject("Conjugar")
22 | UIApplication.topViewController()?.present(mail, animated: true)
23 | }
24 | } else {
25 | return nil
26 | }
27 | }
28 |
29 | private class EmailDelegate: NSObject, MFMailComposeViewControllerDelegate {
30 | func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
31 | controller.dismiss(animated: true)
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Conjugar/InfoCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoCell.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/1/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class InfoCell: UITableViewCell {
12 | static let identifier = "InfoCell"
13 |
14 | @UsesAutoLayout
15 | var heading: UILabel = {
16 | let label = UILabel()
17 | label.textColor = Colors.yellow
18 | label.font = Fonts.boldBody
19 | return label
20 | }()
21 |
22 | required init?(coder aDecoder: NSCoder) {
23 | NSCoder.fatalErrorNotImplemented()
24 | }
25 |
26 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
27 | super.init(style: style, reuseIdentifier: reuseIdentifier)
28 | backgroundColor = Colors.black
29 | addSubview(heading)
30 |
31 | NSLayoutConstraint.activate([
32 | heading.centerXAnchor.constraint(equalTo: centerXAnchor),
33 | heading.centerYAnchor.constraint(equalTo: centerYAnchor)
34 | ])
35 | }
36 |
37 | func configure(heading: String) {
38 | self.heading.text = heading
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ConjugarTests/Utils/TestGameCenterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestGameCenterTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 4/24/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class TestGameCenterTests: XCTestCase {
13 | func testAuthenticate() {
14 | let tgc = TestGameCenter()
15 | Current = World.unitTest
16 | Current.gameCenter = tgc
17 | let dummyVC = UIViewController()
18 |
19 | tgc.authenticate(onViewController: dummyVC, completion: { didAuthenticate in
20 | XCTAssert(didAuthenticate)
21 |
22 | tgc.authenticate(onViewController: dummyVC, completion: { didAuthenticate in
23 | XCTAssertFalse(didAuthenticate)
24 | })
25 | })
26 | }
27 |
28 | func testReportScore() {
29 | // Nothing to test. Exercising for coverage.
30 | let tgc = TestGameCenter()
31 | tgc.reportScore(42)
32 | }
33 |
34 | func testShowLeaderboard() {
35 | // Nothing to test. Exercising for coverage.
36 | let tgc = TestGameCenter()
37 | tgc.showLeaderboard()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Conjugar/Fonts.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Fonts.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/2/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct Fonts {
12 | static let heading = UIFont(name: "AvenirNext-Bold", size: 24.0) ?? safeFont
13 | static let subheading = UIFont(name: "AvenirNext-Demibold", size: 18.0) ?? safeFont
14 | static let largeCell = UIFont(name: "AvenirNext-Regular", size: 24.0) ?? safeFont
15 | static let regularCell = UIFont(name: "AvenirNext-Bold", size: 18.0) ?? safeFont
16 | static let smallCell = UIFont(name: "AvenirNext-Regular", size: 18.0) ?? safeFont
17 | static let button = UIFont(name: "AvenirNext-Demibold", size: 24.0) ?? safeFont
18 | static let label = UIFont(name: "AvenirNext-Demibold", size: 18.0) ?? safeFont
19 | static let body = UIFont(name: "AvenirNext-Regular", size: 16.0) ?? safeFont
20 | static let smallBody = UIFont(name: "AvenirNext-Demibold", size: 12.0) ?? safeFont
21 | static let boldBody = UIFont(name: "AvenirNext-Bold", size: 16.0) ?? safeFont
22 | private static let safeFont = UIFont.systemFont(ofSize: 18.0)
23 | }
24 |
--------------------------------------------------------------------------------
/Conjugar/InfoUIV.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoUIV.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/30/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class InfoUIV: UIView {
12 | @UsesAutoLayout
13 | var info: UITextView = {
14 | var textView = UITextView()
15 | textView.backgroundColor = Colors.black
16 | textView.textColor = Colors.yellow
17 | textView.tintColor = Colors.blue
18 | textView.isEditable = false
19 | return textView
20 | }()
21 |
22 | override init(frame: CGRect) {
23 | super.init(frame: frame)
24 | addSubview(info)
25 |
26 | NSLayoutConstraint.activate([
27 | info.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
28 | info.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
29 | info.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
30 | info.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing)
31 | ])
32 | }
33 |
34 | required init(coder aDecoder: NSCoder) {
35 | NSCoder.fatalErrorNotImplemented()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ConjugarTests/Utils/UIAlertControllerExtensionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIAlertControllerExtensionTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/3/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import UIKit
11 | @testable import Conjugar
12 |
13 | class UIAlertControllerExtensionTests: XCTestCase, InfoDelegate {
14 | func testShowMessage() {
15 | Current = World.unitTest
16 | let ivc = InfoVC(infoString: NSAttributedString(string: "🍕"), infoDelegate: self)
17 |
18 | guard let window = UIApplication.shared.connectedScenes
19 | .filter({$0.activationState == .foregroundActive})
20 | .map({$0 as? UIWindowScene})
21 | .compactMap({$0})
22 | .first?.windows
23 | .filter({$0.isKeyWindow}).first else {
24 | XCTFail("Could not create window.")
25 | return
26 | }
27 |
28 | window.rootViewController = ivc
29 |
30 | ivc.viewWillAppear(true)
31 | UIAlertController.showMessage("", title: "", okTitle: "", onViewController: ivc)
32 | XCTAssert(ivc.presentedViewController is UIAlertController)
33 | }
34 |
35 | func infoSelectionDidChange(newHeading: String) { }
36 | }
37 |
--------------------------------------------------------------------------------
/Conjugar/UIApplicationExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIApplicationExtensions.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 12/17/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIApplication {
12 | class func topViewController(_ base: UIViewController? = UIApplication.shared.compatibilityWindow?.rootViewController) -> UIViewController? {
13 | if let nav = base as? UINavigationController {
14 | return topViewController(nav.visibleViewController)
15 | }
16 | if let tab = base as? UITabBarController {
17 | if let selected = tab.selectedViewController {
18 | return topViewController(selected)
19 | }
20 | }
21 | if let presented = base?.presentedViewController {
22 | return topViewController(presented)
23 | }
24 | return base
25 | }
26 |
27 | // https://stackoverflow.com/a/57169802/8248798
28 | var compatibilityWindow: UIWindow? {
29 | return UIApplication.shared.connectedScenes
30 | .filter({$0.activationState == .foregroundActive})
31 | .map({$0 as? UIWindowScene})
32 | .compactMap({$0})
33 | .first?.windows
34 | .filter({$0.isKeyWindow}).first
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ConjugarTests/UIViews/ConjugationCellTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConjugationCellTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/22/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class ConjugationCellTests: XCTestCase {
13 | func testConjugationCell() {
14 | let cell = ConjugationCell(style: .default, reuseIdentifier: "cell")
15 | XCTAssertEqual(cell.conjugation.textColor, Colors.yellow)
16 | XCTAssertEqual(cell.conjugation.font, Fonts.smallCell)
17 |
18 | cell.configure(tense: .pretérito, personNumber: .secondSingularTú, conjugation: "fuistes")
19 | XCTAssertEqual(cell.conjugation.text, "tú fuistes")
20 |
21 | cell.configure(tense: .imperativoPositivo, personNumber: .secondSingularTú, conjugation: "se")
22 | XCTAssertEqual(cell.conjugation.text, "¡se!")
23 |
24 | cell.configure(tense: .imperativoNegativo, personNumber: .secondSingularTú, conjugation: "no seas")
25 | XCTAssertEqual(cell.conjugation.text, "¡no seas!")
26 |
27 | cell.configure(tense: .imperativoNegativo, personNumber: .secondSingularTú, conjugation: Conjugator.defective)
28 | XCTAssertEqual(cell.conjugation.text, "")
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Conjugar/UIViewExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewExtension.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 8/12/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIView {
12 | func pulsate() {
13 | let duration: TimeInterval = 0.3
14 | let scale: CGFloat = 0.6
15 | UIView.animate(withDuration: duration, animations: {
16 | self.transform = CGAffineTransform.identity.scaledBy(x: scale, y: scale)
17 | }, completion: { _ in
18 | UIView.animate(withDuration: duration, animations: {
19 | self.transform = CGAffineTransform.identity
20 | })
21 | })
22 | }
23 | }
24 |
25 | extension UIView {
26 | func setAccessibilityLabelInSpanish(_ label: String, region: String = Current.settings.region.accent) {
27 | setAccessibilityLabel(label, spokenInLanguage: "es_\(region)")
28 | }
29 |
30 | private func setAccessibilityLabel(_ label: String, spokenInLanguage languageCode: String) {
31 | let attributes: [NSAttributedString.Key: Any] = [
32 | .accessibilitySpeechLanguage: languageCode
33 | ]
34 | let attributedLabel = NSAttributedString(string: label, attributes: attributes)
35 | accessibilityAttributedLabel = attributedLabel
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/privacyPolicy.md:
--------------------------------------------------------------------------------
1 | Privacy Policy
2 | ===================
3 |
4 | Conjugar uses the Amazon Web Services™ analytics product, Pinpoint™, to track certain information. This information consists of:
5 |
6 | * Each screen visited
7 | * The act of starting a quiz
8 | * The act of finishing a quiz, with score
9 | * Game Center™ authentication
10 | * App version
11 | * Unique installs
12 | * Device type (iPhone or iPad)
13 | * Country of user
14 |
15 | Conjugar does _not_ track any personally identifiable user information.
16 |
17 | As demonstrated in the following examples, analytics inform future development of Conjugar.
18 |
19 | At present, quizzes have fifty verbs. The goal of this length is to present a mix of all tenses for the current difficulty level with a mix of regular and irregular verbs. But if, for example, most users are starting but not finishing quizzes, Conjugar's developer might consider shortening the quiz length.
20 |
21 | Conjugar has Game Center™ integration. Use of this integration is optional. If most users are not authenticating Game Center™, Conjugar's developer might consider removing the integration.
22 |
23 | Please direct any questions about or feedback on this privacy policy to Conjugar's [developer](mailto:vermontcoder@gmail.com).
24 |
--------------------------------------------------------------------------------
/ConjugarTests/UIViews/ResultCellTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultCellTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/16/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class ResultCellTests: XCTestCase {
13 | func testResultCell() {
14 | let cell = ResultCell(style: .default, reuseIdentifier: "cell")
15 |
16 | cell.configure(verb: "dar", tense: .presenteDeIndicativo, personNumber: .firstSingular, correctAnswer: "doy", proposedAnswer: "doy")
17 | XCTAssertEqual(cell.verb.text, "dar")
18 | XCTAssertEqual(cell.tensePersonNumber.text, "presente de indicativo, 1S")
19 | XCTAssertEqual(cell.correctAnswer.text, "doy")
20 | XCTAssertEqual(cell.proposedAnswer.text, "doy")
21 | XCTAssert(cell.proposedAnswer.textColor == Colors.yellow)
22 |
23 | cell.configure(verb: "dar", tense: .presenteDeIndicativo, personNumber: .firstSingular, correctAnswer: "doy", proposedAnswer: "do")
24 | XCTAssertEqual(cell.verb.text, "dar")
25 | XCTAssertEqual(cell.tensePersonNumber.text, "presente de indicativo, 1S")
26 | XCTAssertEqual(cell.correctAnswer.text, "doy")
27 | XCTAssertEqual(cell.proposedAnswer.text, "do")
28 | XCTAssert(cell.proposedAnswer.textColor == Colors.blue)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Conjugar/Modifiers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Modifiers.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 11/3/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct HeadingLabel: ViewModifier {
12 | func body(content: Content) -> some View {
13 | content
14 | .font(.heading)
15 | .foregroundColor(Color(Colors.yellow))
16 | }
17 | }
18 |
19 | struct SubheadingLabel: ViewModifier {
20 | func body(content: Content) -> some View {
21 | content
22 | .font(.subheading)
23 | .foregroundColor(Color(Colors.yellow))
24 | }
25 | }
26 |
27 | struct BodyLabel: ViewModifier {
28 | func body(content: Content) -> some View {
29 | content
30 | .font(.smallBody)
31 | .foregroundColor(Color(Colors.yellow))
32 | .padding(.horizontal, Layout.defaultHorizontalMargin)
33 | }
34 | }
35 |
36 | struct StandardButton: ViewModifier {
37 | func body(content: Content) -> some View {
38 | content
39 | .font(.button)
40 | .foregroundColor(Color(Colors.red))
41 | }
42 | }
43 |
44 | struct SegmentedPicker: ViewModifier {
45 | func body(content: Content) -> some View {
46 | content
47 | .pickerStyle(SegmentedPickerStyle())
48 | .padding(.horizontal, Layout.defaultHorizontalMargin)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Conjugar/VerbCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerbCell.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 4/10/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class VerbCell: UITableViewCell {
12 | static let identifier = "VerbCell"
13 |
14 | @UsesAutoLayout
15 | var verb: UILabel = {
16 | let label = UILabel()
17 | label.textColor = Colors.yellow
18 | label.font = Fonts.largeCell
19 | label.textAlignment = .center
20 | label.adjustsFontSizeToFitWidth = true
21 | return label
22 | }()
23 |
24 | required init?(coder aDecoder: NSCoder) {
25 | NSCoder.fatalErrorNotImplemented()
26 | }
27 |
28 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
29 | super.init(style: style, reuseIdentifier: reuseIdentifier)
30 | backgroundColor = Colors.black
31 | addSubview(verb)
32 |
33 | NSLayoutConstraint.activate([
34 | verb.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.defaultSpacing),
35 | verb.trailingAnchor.constraint(equalTo: trailingAnchor, constant: Layout.defaultSpacing * -1.0),
36 | verb.centerYAnchor.constraint(equalTo: centerYAnchor)
37 | ])
38 | }
39 |
40 | func configure(verb: String) {
41 | self.verb.text = verb
42 | self.verb.setAccessibilityLabelInSpanish(verb)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ConjugarTests/Utils/RatingsFetcherTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RatingsFetcherTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 4/26/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class RatingsFetcherTests: XCTestCase {
13 | override func setUp() {
14 | Current = World.unitTest
15 | }
16 |
17 | func testNoReviews() {
18 | testDescription(count: 0, expectedDescription: "No one has rated this version of Conjugar. ¡Sé la primera o el primero!")
19 | }
20 |
21 | func testOneReview() {
22 | testDescription(count: 1, expectedDescription: "There is one rating for this version of Conjugar. Add yours!")
23 | }
24 |
25 | func testManyReviews() {
26 | testDescription(count: 42, expectedDescription: "There are 42 ratings for this version of Conjugar. Add yours!")
27 | }
28 |
29 | private func testDescription(count: Int, expectedDescription: String) {
30 | Current.session = URLSession.stubSession(ratingsCount: count)
31 | let expectatiön = expectation(description: "testDescription")
32 | RatingsFetcher.fetchRatingsDescription { actualDescription in
33 | XCTAssertEqual(actualDescription, expectedDescription)
34 | expectatiön.fulfill()
35 | }
36 | let timeout: TimeInterval = 0.5
37 | wait(for: [expectatiön], timeout: timeout)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Conjugar/SoundPlayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoundPlayer.swift
3 | // Conjugar
4 | //
5 | // Created by Josh Adams on 11/18/15.
6 | // Copyright © 2015 Josh Adams. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 |
11 | class SoundPlayer {
12 | private static let soundPlayer = SoundPlayer()
13 | private var sounds: [String: AVAudioPlayer]
14 | private static let soundExtension = "mp3"
15 |
16 | private init () {
17 | sounds = Dictionary()
18 | do {
19 | try AVAudioSession.sharedInstance().setCategory(.playback) // was ambient
20 | } catch let error as NSError {
21 | print("\(error.localizedDescription)")
22 | }
23 | }
24 |
25 | static func play(_ sound: Sound) {
26 | if soundPlayer.sounds[sound.rawValue] == nil {
27 | if let audioUrl = Bundle.main.url(forResource: sound.rawValue, withExtension: soundExtension) {
28 | do {
29 | try soundPlayer.sounds[sound.rawValue] = AVAudioPlayer.init(contentsOf: audioUrl)
30 | } catch let error as NSError {
31 | print("\(error.localizedDescription)")
32 | }
33 | }
34 | }
35 | soundPlayer.sounds[sound.rawValue]?.play()
36 | }
37 |
38 | static func playRandomApplause() {
39 | let applauses: [Sound] = [.applause1, .applause2, .applause3]
40 | let applauseIndex = Int.random(in: 0 ... (applauses.count - 1))
41 | SoundPlayer.play(applauses[applauseIndex])
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Conjugar/TenseCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TenseCell.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 5/7/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TenseCell: UITableViewCell {
12 | static let identifier = "TenseCell"
13 |
14 | @UsesAutoLayout
15 | var tense: UILabel = {
16 | let label = UILabel()
17 | label.textColor = Colors.red
18 | label.font = Fonts.regularCell
19 | label.textAlignment = .center
20 | label.adjustsFontSizeToFitWidth = true
21 | return label
22 | }()
23 |
24 | required init?(coder aDecoder: NSCoder) {
25 | NSCoder.fatalErrorNotImplemented()
26 | }
27 |
28 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
29 | super.init(style: style, reuseIdentifier: reuseIdentifier)
30 | backgroundColor = Colors.black
31 | selectionStyle = .none
32 | addSubview(tense)
33 | accessibilityTraits = accessibilityTraits.union(.header)
34 |
35 | NSLayoutConstraint.activate([
36 | tense.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.defaultSpacing),
37 | tense.trailingAnchor.constraint(equalTo: trailingAnchor, constant: Layout.defaultSpacing * -1.0),
38 | tense.centerYAnchor.constraint(equalTo: centerYAnchor)
39 | ])
40 | }
41 |
42 | func configure(tense: String) {
43 | self.tense.text = tense
44 | self.tense.setAccessibilityLabelInSpanish(tense)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Conjugar/Utterer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utterer.swift
3 | // Conjugar
4 | //
5 | // Created by Josh Adams on 11/18/15.
6 | // Copyright © 2015 Josh Adams. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 |
11 | class Utterer {
12 | private static let synth = AVSpeechSynthesizer()
13 | private static let rate: Float = 0.5
14 | private static let pitchMultiplier: Float = 0.8
15 | private static var settings: Settings?
16 |
17 | static func setup(settings: Settings) {
18 | Utterer.settings = settings
19 |
20 | let session = AVAudioSession.sharedInstance()
21 | do {
22 | try session.setCategory(.playback, options: .mixWithOthers)
23 | } catch let error as NSError {
24 | print("\(error.localizedDescription)")
25 | }
26 | utter("")
27 | }
28 |
29 | static func utter(_ thingToUtter: String, locale: String? = nil) {
30 | guard let settings = settings else {
31 | fatalError("settings not initialized. Accent not inferrable.")
32 | }
33 | let utterance = AVSpeechUtterance(string: thingToUtter)
34 | utterance.rate = Utterer.rate
35 | if let locale = locale {
36 | utterance.voice = AVSpeechSynthesisVoice(language: locale)
37 | } else {
38 | utterance.voice = AVSpeechSynthesisVoice(language: "es-" + settings.region.accent)
39 | }
40 | utterance.pitchMultiplier = Utterer.pitchMultiplier
41 | synth.speak(utterance)
42 | SoundPlayer.play(.silence) // https://forums.developer.apple.com/thread/23160
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Conjugar/ReviewPrompter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewPrompter.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 1/5/18.
6 | // Copyright © 2018 Josh Adams. All rights reserved.
7 | //
8 |
9 | import StoreKit
10 |
11 | struct ReviewPrompter: ReviewPromptable {
12 | static let promptModulo = 9
13 | static let promptInterval: TimeInterval = 60 * 60 * 24 * 180
14 | private let settings: Settings
15 | private let now: Date
16 | private let requestReview: () -> ()
17 | private static let defaultRequestReview: () -> Void = {
18 | if let scene = UIApplication.shared.connectedScenes.first(
19 | where: { $0.activationState == .foregroundActive }
20 | ) as? UIWindowScene {
21 | DispatchQueue.main.async {
22 | SKStoreReviewController.requestReview(in: scene)
23 | }
24 | }
25 | }
26 |
27 | init(settings: Settings = Settings(getterSetter: UserDefaultsGetterSetter()), now: Date = Date(), requestReview: @escaping () -> () = ReviewPrompter.defaultRequestReview) {
28 | self.settings = settings
29 | self.now = now
30 | self.requestReview = requestReview
31 | }
32 |
33 | func promptableActionHappened() {
34 | var actionCount = settings.promptActionCount
35 | actionCount += 1
36 | settings.promptActionCount = actionCount
37 | let lastReviewPromptDate = settings.lastReviewPromptDate
38 | if actionCount % ReviewPrompter.promptModulo == 0 && now.timeIntervalSince(lastReviewPromptDate) >= ReviewPrompter.promptInterval {
39 | requestReview()
40 | settings.lastReviewPromptDate = now
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/VerbVCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerbVCTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 9/2/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class VerbVCTests: XCTestCase {
13 | func testVerbVC() {
14 | var analytic = ""
15 | Current = World.unitTest
16 | Current.analytics = TestAnalyticsService(fire: { fired in analytic = fired })
17 |
18 | let vvc = VerbVC(verb: "maltear")
19 |
20 | XCTAssertNotNil(vvc)
21 | XCTAssertNotNil(vvc.verbView)
22 | vvc.viewWillAppear(true)
23 | XCTAssertEqual(analytic, "visited viewController: \(VerbVC.self) ")
24 | }
25 |
26 | func testVerbTypes() {
27 | let arText = "Regular AR"
28 | let erText = "Regular ER"
29 | let irText = "Regular IR"
30 | let defectiveText = "Defective"
31 | let notDefectiveText = "Not Defective"
32 | let parentText = "Irreg. ☛ conocer"
33 | let irregularText = "Irregular"
34 |
35 | [
36 | ("maltear", arText, notDefectiveText),
37 | ("comer", erText, notDefectiveText),
38 | ("subir", irText, notDefectiveText),
39 | ("gustar", arText, defectiveText),
40 | ("reconocer", parentText, notDefectiveText),
41 | ("ser", irregularText, notDefectiveText)
42 | ].forEach {
43 | let vvc = VerbVC(verb: $0.0)
44 | vvc.viewWillAppear(true)
45 | let vv = vvc.verbView
46 | XCTAssertEqual(vv.parentOrType.text, $0.1)
47 | XCTAssertEqual(vv.defectuoso.text, $0.2)
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/privacyPolicy2.html:
--------------------------------------------------------------------------------
1 | Privacy Policy
2 |
3 | Conjugar uses the Amazon Web Services™ analytics product, Pinpoint™, to track certain information. This information consists of:
4 |
5 |
6 | Each screen visited
7 |
8 | The act of starting a quiz
9 |
10 | The act of finishing a quiz, with score
11 |
12 | The act of quitting a quiz, with score and progress
13 |
14 | Game Center™ authentication
15 |
16 | App version
17 |
18 | Unique installs
19 |
20 | Device type and model
21 |
22 | Country of user
23 |
24 | Language of user
25 |
26 | Communications presented to user and action taken
27 |
28 |
29 | Conjugar does not track any personally identifiable user information.
30 |
31 | As demonstrated in the following examples, analytics inform future development of Conjugar.
32 |
33 | At present, quizzes have fifty verbs. The goal of this length is to present a mix of all tenses for the current difficulty level with a mix of regular and irregular verbs. But if, for example, most users are starting but not finishing quizzes, Conjugar's developer might consider shortening the quiz length.
34 |
35 | Conjugar has Game Center™ integration. Use of this integration is optional. If most users are not authenticating Game Center™, Conjugar's developer might consider removing the integration.
36 |
37 | Please direct any questions about or feedback on this privacy policy to Conjugar's developer .
38 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/BrowseVerbsVCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BrowseVerbsVCTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 8/27/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class BrowseVerbsVCTests: XCTestCase {
13 | func testBrowseVerbsVC() {
14 | var analytic = ""
15 | Current.analytics = TestAnalyticsService(fire: { fired in analytic = fired })
16 | Current.settings = Settings(getterSetter: DictionaryGetterSetter())
17 |
18 | let bvvc = BrowseVerbsVC()
19 |
20 | let nc = MockNavigationC(rootViewController: bvvc)
21 |
22 | XCTAssertNotNil(bvvc)
23 | bvvc.viewWillAppear(true)
24 | XCTAssertEqual(analytic, "visited viewController: \(BrowseVerbsVC.self) ")
25 |
26 | let irregularVerbCount = Conjugator.shared.irregularVerbs.count
27 | let regularVerbCount = Conjugator.shared.regularVerbs.count
28 | let combinedVerbCount = irregularVerbCount + regularVerbCount
29 |
30 | let bvv = bvvc.browseVerbsView
31 | [(0, irregularVerbCount), (1, regularVerbCount), (2, combinedVerbCount)].forEach {
32 | bvv.filterControl.selectedSegmentIndex = $0.0
33 | XCTAssertEqual(bvvc.tableView(UITableView(), numberOfRowsInSection: 0), $0.1)
34 | }
35 |
36 | bvv.filterControl.selectedSegmentIndex = 0
37 | bvvc.valueChanged(bvv.filterControl)
38 | XCTAssertEqual(bvvc.tableView(UITableView(), numberOfRowsInSection: 0), irregularVerbCount)
39 |
40 | bvvc.tableView(UITableView(), didSelectRowAt: IndexPath(row: 0, section: 0))
41 | XCTAssert(nc.pushedViewController is VerbVC)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Conjugar/PersonNumber.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PersonNumber.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 3/31/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | enum PersonNumber: String, CaseIterable {
10 | case firstSingular = "fs"
11 | case firstPlural = "fp"
12 | case secondSingularTú = "ss"
13 | case secondSingularVos = "sv"
14 | case secondPlural = "sp"
15 | case thirdSingular = "ts"
16 | case thirdPlural = "tp"
17 | case none = "no"
18 |
19 | var pronoun: String {
20 | switch self {
21 | case .firstSingular:
22 | return "yo"
23 | case .secondSingularTú:
24 | return "tú"
25 | case .secondSingularVos:
26 | return "vos"
27 | case .thirdSingular:
28 | return "él"
29 | case .firstPlural:
30 | return "nosotros"
31 | case .secondPlural:
32 | return "vosotros"
33 | case .thirdPlural:
34 | return "ellas"
35 | case .none:
36 | return "none"
37 | }
38 | }
39 |
40 | var shortDisplayName: String {
41 | switch self {
42 | case .firstSingular:
43 | return "1S"
44 | case .secondSingularTú:
45 | return "2S"
46 | case .secondSingularVos:
47 | return "2SV"
48 | case .thirdSingular:
49 | return "3S"
50 | case .firstPlural:
51 | return "1P"
52 | case .secondPlural:
53 | return "2P"
54 | case .thirdPlural:
55 | return "3P"
56 | case .none:
57 | return "none"
58 | }
59 | }
60 |
61 | static let actualPersonNumbers: [PersonNumber] = [.firstSingular, .secondSingularTú, .secondSingularVos, .thirdSingular, .firstPlural, .secondPlural, .thirdPlural]
62 | }
63 |
--------------------------------------------------------------------------------
/Conjugar/AWSAnalyticsService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AWSAnalyticsService.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 10/8/18.
6 | // Copyright © 2018 Joshua Adams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AWSPinpoint
11 |
12 | class AWSAnalyticsService: NSObject, AnalyticsServiceable {
13 | var pinpoint: AWSPinpoint
14 |
15 | override init() {
16 | let config = AWSPinpointConfiguration.defaultPinpointConfiguration(launchOptions: nil)
17 | pinpoint = AWSPinpoint(configuration: config)
18 | super.init()
19 | recordCustomProfileDemographics()
20 | // AWSDDLog.sharedInstance.logLevel = .verbose
21 | // AWSDDLog.add(AWSDDTTYLogger.sharedInstance)
22 | }
23 |
24 | func recordEvent(_ eventName: String, parameters: [String: String]? = nil, metrics: [String: Double]? = nil) {
25 | let event = pinpoint.analyticsClient.createEvent(withEventType: eventName)
26 | if let parameters = parameters {
27 | for (key, value) in parameters {
28 | event.addAttribute(value, forKey: key)
29 | }
30 | }
31 | if let metrics = metrics {
32 | for (key, value) in metrics {
33 | event.addMetric(NSNumber(value: value), forKey: key)
34 | }
35 | }
36 | pinpoint.analyticsClient.record(event)
37 | pinpoint.analyticsClient.submitEvents()
38 | }
39 |
40 | private func recordCustomProfileDemographics() {
41 | let profile: AWSPinpointEndpointProfile = (pinpoint.targetingClient.currentEndpointProfile())
42 | profile.demographic?.model = UIDevice.current.modelName
43 | profile.demographic?.platformVersion = UIDevice.current.systemVersion
44 | pinpoint.targetingClient.update(profile)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Conjugar/InfoVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoVC.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/1/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class InfoVC: UIViewController, UITextViewDelegate {
12 | private weak var infoDelegate: InfoDelegate?
13 | private let infoString: NSAttributedString
14 |
15 | var infoView: InfoUIV {
16 | if let castedView = view as? InfoUIV {
17 | return castedView
18 | } else {
19 | fatalError(fatalCastMessage(view: InfoUIV.self))
20 | }
21 | }
22 |
23 | init(infoString: NSAttributedString, infoDelegate: InfoDelegate) {
24 | self.infoString = infoString
25 | self.infoDelegate = infoDelegate
26 | super.init(nibName: nil, bundle: nil)
27 | }
28 |
29 | required init?(coder aDecoder: NSCoder) {
30 | NSCoder.fatalErrorNotImplemented()
31 | }
32 |
33 | override func loadView() {
34 | let infoView: InfoUIV
35 | infoView = InfoUIV(frame: UIScreen.main.bounds)
36 | infoView.info.attributedText = infoString
37 | infoView.info.delegate = self
38 | infoView.info.contentOffset = CGPoint.zero
39 | view = infoView
40 | }
41 |
42 | override func viewWillAppear(_ animated: Bool) {
43 | super.viewWillAppear(animated)
44 | Current.analytics.recordVisitation(viewController: "\(InfoVC.self)")
45 | }
46 |
47 | func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
48 | let http = "http"
49 | if URL.absoluteString.prefix(http.count) == http {
50 | return true
51 | } else {
52 | navigationController?.popViewController(animated: true)
53 | infoDelegate?.infoSelectionDidChange(newHeading: URL.absoluteString)
54 | return false
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/InfoVCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InfoVCTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/23/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import UIKit
11 | @testable import Conjugar
12 |
13 | class InfoVCTests: XCTestCase, InfoDelegate {
14 | var newHeading = ""
15 |
16 | func infoSelectionDidChange(newHeading: String) {
17 | self.newHeading = newHeading
18 | }
19 |
20 | func testInfoVC() {
21 | var analytic = ""
22 | Current = World.unitTest
23 | Current.analytics = TestAnalyticsService(fire: { fired in analytic = fired })
24 |
25 | let urlString = "https://racecondition.software"
26 | guard let url = URL(string: urlString) else {
27 | XCTFail("Could not create URL.")
28 | return
29 | }
30 |
31 | let nonURLInfoString = "info"
32 | guard let nonURLInfoURL = URL(string: nonURLInfoString) else {
33 | XCTFail("Could not create nonURLInfoURL.")
34 | return
35 | }
36 |
37 | let ivc = InfoVC(infoString: NSAttributedString(string: "\(nonURLInfoString)\(url)"), infoDelegate: self)
38 |
39 | XCTAssertNotNil(ivc)
40 | XCTAssertNotNil(ivc.infoView)
41 | ivc.viewWillAppear(true)
42 | XCTAssertEqual(analytic, "visited viewController: \(InfoVC.self) ")
43 |
44 | XCTAssertEqual(newHeading, "")
45 | var shouldInteract = ivc.textView(UITextView(), shouldInteractWith: nonURLInfoURL, in: NSRange(location: 0, length: nonURLInfoString.count))
46 | XCTAssertFalse(shouldInteract)
47 | XCTAssertEqual(newHeading, nonURLInfoString)
48 |
49 | shouldInteract = ivc.textView(UITextView(), shouldInteractWith: url, in: NSRange(location: nonURLInfoString.count, length: "\(url)".count))
50 | XCTAssert(shouldInteract)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/CommunViewModelTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommunViewModelTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 12/21/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | @testable import Conjugar
10 | import XCTest
11 |
12 | class CommunViewModelTests: XCTestCase {
13 | func testProperties() {
14 | var didTapAction = false
15 |
16 | let settings = Settings(getterSetter: DictionaryGetterSetter())
17 | let gameCenter = TestGameCenter(isAuthenticated: false)
18 | let analytics = TestAnalyticsService()
19 | let quiz = Quiz(settings: settings, gameCenter: gameCenter, shouldShuffle: false)
20 | let communGetter = StubCommunGetter()
21 |
22 | Current = World(
23 | analytics: analytics,
24 | reviewPrompter: TestReviewPrompter(),
25 | gameCenter: gameCenter,
26 | settings: settings,
27 | quiz: quiz,
28 | session: URLSession.stubSession(ratingsCount: 0),
29 | communGetter: communGetter,
30 | locale: StubLocale(languageCode: "en", regionCode: "US")
31 | )
32 |
33 | let actionType = Commun.CommunType.website(actionTitle: ["en": "🐬"], cancelTitle: ["en": "🐉"], action: { didTapAction = true })
34 | let commun = Commun(title: ["en": "🐋"], image: UIImage(), imageLabel: ["en": "🍕"], content: ["en": "🏴"], type: actionType, identifier: 0)
35 | let vm = CommunViewModel(commun: commun)
36 |
37 | XCTAssertFalse(didTapAction)
38 | vm.action()
39 | XCTAssert(didTapAction)
40 | XCTAssertEqual(vm.title, "🐋")
41 | XCTAssertEqual(vm.content, "🏴")
42 | XCTAssertEqual(vm.imageLabel, "🍕")
43 | XCTAssertEqual(vm.okayTitle, "")
44 | XCTAssertEqual(vm.cancelTitle, "🐉")
45 | XCTAssertEqual(vm.actionTitle, "🐬")
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/ResultsVCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultsVCTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/24/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class ResultsVCTests: XCTestCase {
13 | func testResultsVC() {
14 | var analytic = ""
15 | let settings = Settings(getterSetter: DictionaryGetterSetter())
16 | settings.userRejectedGameCenter = true
17 | let gameCenter = TestGameCenter(isAuthenticated: true)
18 | let analytics = TestAnalyticsService(fire: { fired in analytic = fired })
19 | let quiz = Quiz(settings: settings, gameCenter: gameCenter, shouldShuffle: false)
20 | let fakeRatingsCount = 42
21 |
22 | Current = World(
23 | analytics: analytics,
24 | reviewPrompter: TestReviewPrompter(),
25 | gameCenter: gameCenter,
26 | settings: settings,
27 | quiz: quiz,
28 | session: URLSession.stubSession(ratingsCount: fakeRatingsCount),
29 | communGetter: StubCommunGetter(),
30 | locale: StubLocale(languageCode: "en", regionCode: "US")
31 | )
32 |
33 | let rvc = ResultsVC()
34 |
35 | XCTAssertNotNil(rvc)
36 | XCTAssertNotNil(rvc.resultsView)
37 | rvc.viewWillAppear(true)
38 | XCTAssertEqual(analytic, "visited viewController: \(ResultsVC.self) ")
39 |
40 | quiz.start()
41 | let expectedQuestionCount = 50
42 | (0 ..< expectedQuestionCount).forEach { _ in
43 | let wrongAnswer = "🥥"
44 | _ = quiz.process(proposedAnswer: wrongAnswer)
45 | }
46 |
47 | XCTAssertEqual(rvc.tableView(rvc.resultsView.table, numberOfRowsInSection: 0), expectedQuestionCount)
48 |
49 | let cell = rvc.tableView(rvc.resultsView.table, cellForRowAt: IndexPath(row: 0, section: 0))
50 | XCTAssert(cell is ResultCell)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/BrowseInfoVCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BrowseInfoVCTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 9/2/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class BrowseInfoVCTests: XCTestCase {
13 | func testBrowseInfoVC() {
14 | var analytic = ""
15 | let settings = Settings(getterSetter: DictionaryGetterSetter())
16 | Current.analytics = TestAnalyticsService(fire: { fired in analytic = fired })
17 | Current.settings = settings
18 |
19 | let bivc = BrowseInfoVC()
20 | let nc = MockNavigationC(rootViewController: bivc)
21 |
22 | XCTAssertNotNil(bivc)
23 | bivc.viewWillAppear(true)
24 | XCTAssertEqual(analytic, "visited viewController: \(BrowseInfoVC.self) ")
25 | XCTAssertEqual(bivc.tableView(UITableView(), numberOfRowsInSection: 0), 28)
26 |
27 | let biv = bivc.browseInfoView
28 |
29 | let easyCount = 9
30 | let easyModerateCount = 17
31 | let allCount = 28
32 |
33 | [(0, easyCount), (1, easyModerateCount), (2, allCount)].forEach {
34 | biv.difficultyControl.selectedSegmentIndex = $0.0
35 | XCTAssertEqual(bivc.tableView(UITableView(), numberOfRowsInSection: 0), $0.1)
36 | }
37 |
38 | bivc.tableView(UITableView(), didSelectRowAt: IndexPath(row: 0, section: 0))
39 | XCTAssert(nc.pushedViewController is InfoVC)
40 | nc.popViewController(animated: false)
41 |
42 | [(0, Difficulty.easy), (1, .moderate), (2, .difficult)].forEach {
43 | biv.difficultyControl.selectedSegmentIndex = $0.0
44 | bivc.difficultyChanged(biv.difficultyControl)
45 | XCTAssertEqual(settings.infoDifficulty, $0.1)
46 | }
47 |
48 | bivc.infoSelectionDidChange(newHeading: "Terminology")
49 | XCTAssert(nc.pushedViewController is InfoVC)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Conjugar/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleSpokenName
6 | con hoo gar
7 | ITSAppUsesNonExemptEncryption
8 |
9 | CFBundleDevelopmentRegion
10 | en
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | $(MARKETING_VERSION)
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 | arm64
33 |
34 | UIRequiresFullScreen
35 |
36 | UIStatusBarStyle
37 | UIStatusBarStyleLightContent
38 | UISupportedInterfaceOrientations
39 |
40 | UIInterfaceOrientationPortrait
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 | UIUserInterfaceStyle
50 | Dark
51 | UIViewControllerBasedStatusBarAppearance
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/Conjugar/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 3/31/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 | var window: UIWindow?
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | configureTabBar()
16 | configureNavBar()
17 |
18 | let uiTestingFlag = "enable-ui-testing"
19 | if CommandLine.arguments.contains(uiTestingFlag) {
20 | Current = World.uiTest(launchArguments: CommandLine.arguments)
21 | }
22 |
23 | Utterer.setup(settings: Current.settings)
24 | let mainTabBarVC = MainTabBarVC()
25 |
26 | window = UIWindow(frame: UIScreen.main.bounds)
27 | window?.rootViewController = mainTabBarVC
28 | window?.makeKeyAndVisible()
29 |
30 | return true
31 | }
32 |
33 | private func configureTabBar() {
34 | UITabBar.appearance().barTintColor = UIColor.black
35 | UITabBar.appearance().tintColor = Colors.yellow
36 | }
37 |
38 | private func configureNavBar() {
39 | UINavigationBar.appearance().barTintColor = UIColor.black
40 | UINavigationBar.appearance().tintColor = Colors.yellow
41 | UINavigationBar.appearance().titleTextAttributes = [NSAttributedString.Key(rawValue: NSAttributedString.Key.foregroundColor.rawValue): Colors.yellow]
42 | }
43 |
44 | func applicationWillResignActive(_ application: UIApplication) {}
45 |
46 | func applicationDidEnterBackground(_ application: UIApplication) {}
47 |
48 | func applicationWillEnterForeground(_ application: UIApplication) {}
49 |
50 | func applicationDidBecomeActive(_ application: UIApplication) {
51 | Current.analytics.recordBecameActive()
52 | }
53 |
54 | func applicationWillTerminate(_ application: UIApplication) {}
55 | }
56 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/MainTabBarVCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainTabBarVCTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 3/31/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import SwiftUI
11 | @testable import Conjugar
12 |
13 | class MainTabBarVCTests: XCTestCase {
14 | func testMainTabBarVC() {
15 | let mtbvc = MainTabBarVC()
16 | XCTAssertNotNil(mtbvc)
17 |
18 | if let firstNavC = mtbvc.selectedViewController as? UINavigationController {
19 | if let browseVerbsVC = firstNavC.visibleViewController {
20 | if !(browseVerbsVC is BrowseVerbsVC) {
21 | XCTFail("First tab's UINavigationController's visibleViewController is not a BrowseVerbsVC.")
22 | }
23 | }
24 | } else {
25 | XCTFail("First tab is not a UINavigationController.")
26 | }
27 |
28 | mtbvc.selectedIndex = 1
29 | if let secondNavC = mtbvc.selectedViewController as? UINavigationController {
30 | if let quizVC = secondNavC.visibleViewController {
31 | if !(quizVC is QuizVC) {
32 | XCTFail("Second tab's UINavigationController's visibleViewController is not a QuizVC.")
33 | }
34 | }
35 | } else {
36 | XCTFail("Second tab is not a UINavigationController.")
37 | }
38 |
39 | mtbvc.selectedIndex = 2
40 | if let thirdNavC = mtbvc.selectedViewController as? UINavigationController {
41 | if let browseInfoVC = thirdNavC.visibleViewController {
42 | if !(browseInfoVC is BrowseInfoVC) {
43 | XCTFail("Third tab's UINavigationController's visibleViewController is not a BrowseInfoVC.")
44 | }
45 | }
46 | } else {
47 | XCTFail("Third tab is not a UINavigationController.")
48 | }
49 |
50 | mtbvc.selectedIndex = 3
51 | if mtbvc.selectedViewController == nil {
52 | XCTFail("Fourth tab's selectedViewController was nil.")
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/ConjugarTests/Models/PersonNumberTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PersonNumberTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/13/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class PersonNumberTests: XCTestCase {
13 | func testShortDisplayName() {
14 | var personNumber = PersonNumber.firstSingular
15 | XCTAssertEqual(personNumber.shortDisplayName, "1S")
16 | personNumber = .secondSingularTú
17 | XCTAssertEqual(personNumber.shortDisplayName, "2S")
18 | personNumber = .secondSingularVos
19 | XCTAssertEqual(personNumber.shortDisplayName, "2SV")
20 | personNumber = .thirdSingular
21 | XCTAssertEqual(personNumber.shortDisplayName, "3S")
22 | personNumber = .firstPlural
23 | XCTAssertEqual(personNumber.shortDisplayName, "1P")
24 | personNumber = .secondPlural
25 | XCTAssertEqual(personNumber.shortDisplayName, "2P")
26 | personNumber = .thirdPlural
27 | XCTAssertEqual(personNumber.shortDisplayName, "3P")
28 | personNumber = .none
29 | XCTAssertEqual(personNumber.shortDisplayName, "none")
30 | }
31 |
32 | func testPronoun() {
33 | var personNumber = PersonNumber.firstSingular
34 | XCTAssertEqual(personNumber.pronoun, "yo")
35 | personNumber = PersonNumber.secondSingularTú
36 | XCTAssertEqual(personNumber.pronoun, "tú")
37 | personNumber = PersonNumber.secondSingularVos
38 | XCTAssertEqual(personNumber.pronoun, "vos")
39 | personNumber = PersonNumber.thirdSingular
40 | XCTAssertEqual(personNumber.pronoun, "él")
41 | personNumber = PersonNumber.firstPlural
42 | XCTAssertEqual(personNumber.pronoun, "nosotros")
43 | personNumber = PersonNumber.secondPlural
44 | XCTAssertEqual(personNumber.pronoun, "vosotros")
45 | personNumber = PersonNumber.thirdPlural
46 | XCTAssertEqual(personNumber.pronoun, "ellas")
47 | personNumber = PersonNumber.none
48 | XCTAssertEqual(personNumber.pronoun, "none")
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Conjugar/ResultsVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultsVC.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 6/25/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ResultsVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
12 | var resultsView: ResultsUIV {
13 | if let castedView = view as? ResultsUIV {
14 | return castedView
15 | } else {
16 | fatalError(fatalCastMessage(view: ResultsUIV.self))
17 | }
18 | }
19 |
20 | override func loadView() {
21 | let resultsView: ResultsUIV
22 | resultsView = ResultsUIV(frame: UIScreen.main.bounds)
23 | resultsView.setupTable(dataSource: self, delegate: self)
24 | navigationItem.titleView = UILabel.titleLabel(title: Localizations.Results.title)
25 | view = resultsView
26 | }
27 |
28 | override func viewWillAppear(_ animated: Bool) {
29 | super.viewWillAppear(animated)
30 | resultsView.difficulty.text = Current.quiz.lastDifficulty.localizedDifficulty
31 | resultsView.region.text = Current.quiz.lastRegion.localizedRegion
32 | resultsView.score.text = String(Current.quiz.score)
33 | resultsView.time.text = Current.quiz.elapsedTime.timeString
34 | Current.analytics.recordVisitation(viewController: "\(ResultsVC.self)")
35 | }
36 |
37 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
38 | return Current.quiz.questions.count
39 | }
40 |
41 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
42 | guard let cell = resultsView.table.dequeueReusableCell(withIdentifier: ResultCell.identifier) as? ResultCell else {
43 | fatalError("Could not dequeue \(ResultCell.self).")
44 | }
45 | let row = indexPath.row
46 | let question = Current.quiz.questions[row]
47 | cell.configure(verb: question.0, tense: question.1, personNumber: question.2, correctAnswer: Current.quiz.correctAnswers[row], proposedAnswer: Current.quiz.proposedAnswers[row])
48 | return cell
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Conjugar/BrowseVerbsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BrowseVerbsUIV.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/16/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class BrowseVerbsUIV: UIView {
12 | @UsesAutoLayout
13 | var table: UITableView = {
14 | let tableView = UITableView()
15 | tableView.backgroundColor = Colors.black
16 | return tableView
17 | }()
18 |
19 | @UsesAutoLayout
20 | var filterControl: UISegmentedControl = {
21 | let control = UISegmentedControl(items: [
22 | Localizations.Verb.irregular,
23 | Localizations.Verb.regular,
24 | Localizations.bothMasculine
25 | ])
26 | control.selectedSegmentIndex = 0
27 | control.yellowfyText()
28 | return control
29 | }()
30 |
31 | required init(coder aDecoder: NSCoder) {
32 | NSCoder.fatalErrorNotImplemented()
33 | }
34 |
35 | override init(frame: CGRect) {
36 | super.init(frame: frame)
37 | [table, filterControl].forEach {
38 | addSubview($0)
39 | }
40 |
41 | NSLayoutConstraint.activate([
42 | table.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
43 | table.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
44 | table.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
45 | table.bottomAnchor.constraint(equalTo: filterControl.topAnchor, constant: -1.0 * Layout.defaultSpacing),
46 | filterControl.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
47 | filterControl.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
48 | filterControl.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing)
49 | ])
50 | }
51 |
52 | func setupTable(dataSource: UITableViewDataSource, delegate: UITableViewDelegate) {
53 | table.dataSource = dataSource
54 | table.delegate = delegate
55 | table.register(VerbCell.self, forCellReuseIdentifier: VerbCell.identifier)
56 | }
57 |
58 | func reloadTableData() {
59 | table.reloadData()
60 | table.setContentOffset(CGPoint.zero, animated: false)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Conjugar/ConjugationCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConjugationCell.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 5/7/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ConjugationCell: UITableViewCell {
12 | static let identifier = "ConjugationCell"
13 |
14 | @UsesAutoLayout
15 | var conjugation: UILabel = {
16 | let label = UILabel()
17 | label.textColor = Colors.yellow
18 | label.font = Fonts.smallCell
19 | label.textAlignment = .center
20 | label.adjustsFontSizeToFitWidth = true
21 | return label
22 | }()
23 |
24 | required init?(coder aDecoder: NSCoder) {
25 | NSCoder.fatalErrorNotImplemented()
26 | }
27 |
28 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
29 | super.init(style: style, reuseIdentifier: reuseIdentifier)
30 | addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(ConjugationCell.tap(_:))))
31 | selectionStyle = .none
32 | backgroundColor = Colors.black
33 | addSubview(conjugation)
34 |
35 | NSLayoutConstraint.activate([
36 | conjugation.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Layout.defaultSpacing),
37 | conjugation.trailingAnchor.constraint(equalTo: trailingAnchor, constant: Layout.defaultSpacing * -1.0),
38 | conjugation.centerYAnchor.constraint(equalTo: centerYAnchor)
39 | ])
40 | }
41 |
42 | func configure(tense: Tense, personNumber: PersonNumber, conjugation: String) {
43 | var conjugation = conjugation
44 | if conjugation == Conjugator.defective {
45 | self.conjugation.text = ""
46 | } else {
47 | if tense == .imperativoPositivo || tense == .imperativoNegativo {
48 | conjugation = "¡" + conjugation + "!"
49 | } else {
50 | conjugation = personNumber.pronoun + " " + conjugation
51 | }
52 | self.conjugation.attributedText = conjugation.conjugatedString
53 | self.conjugation.setAccessibilityLabelInSpanish(conjugation)
54 | }
55 | }
56 |
57 | @objc func tap(_ sender: UITapGestureRecognizer) {
58 | Utterer.utter(conjugation.attributedText?.string ?? conjugation.text ?? "")
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Conjugar/MainTabBarVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainTabBarVC.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/15/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class MainTabBarVC: UITabBarController {
13 | convenience init() {
14 | self.init(nibName: nil, bundle: nil)
15 |
16 | let browseVerbsNavC = UINavigationController(rootViewController: BrowseVerbsVC())
17 | browseVerbsNavC.tabBarItem = UITabBarItem(
18 | title: Localizations.BrowseVerbs.localizedTitle,
19 | image: UIImage(named: BrowseVerbsVC.englishTitle),
20 | selectedImage: nil
21 | )
22 |
23 | let quizNavC = UINavigationController(rootViewController: QuizVC())
24 | quizNavC.tabBarItem = UITabBarItem(
25 | title: Localizations.Quiz.localizedTitle,
26 | image: UIImage(named: QuizVC.englishTitle),
27 | selectedImage: nil
28 | )
29 |
30 | let settingsVC = UIHostingController(rootView: SettingsView())
31 | Current.parentViewController = settingsVC
32 | settingsVC.tabBarItem = UITabBarItem(
33 | title: Localizations.Settings.localizedTitle,
34 | image: UIImage(named: SettingsView.englishTitle),
35 | selectedImage: nil
36 | )
37 |
38 | let browseInfoNavC = UINavigationController(rootViewController: BrowseInfoVC())
39 | browseInfoNavC.tabBarItem = UITabBarItem(
40 | title: Localizations.BrowseInfo.localizedTitle,
41 | image: UIImage(named: BrowseInfoVC.englishTitle),
42 | selectedImage: nil
43 | )
44 |
45 | viewControllers = [browseVerbsNavC, quizNavC, browseInfoNavC, settingsVC]
46 | }
47 |
48 | override func viewDidLoad() {
49 | super.viewDidLoad()
50 | // Current.communGetter.getCommunication(completion: { [weak self] commun in
51 | // DispatchQueue.main.async {
52 | // let lastCommunIdentifierShown = Current.settings.lastCommunIdentifierShown
53 | // if Current.quiz.quizState != .inProgress && commun.identifier > lastCommunIdentifierShown {
54 | // let communVC = CommunVC(commun: commun)
55 | // communVC.modalPresentationStyle = .fullScreen
56 | // self?.present(communVC, animated: true)
57 | // Current.settings.lastCommunIdentifierShown = commun.identifier
58 | // }
59 | // }
60 | // })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Conjugar/ResultCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultCell.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 6/25/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ResultCell: UITableViewCell {
12 | static let identifier = "ResultCell"
13 |
14 | @UsesAutoLayout private(set) var verb = UILabel()
15 | @UsesAutoLayout private(set) var tensePersonNumber = UILabel()
16 | @UsesAutoLayout private(set) var correctAnswer = UILabel()
17 | @UsesAutoLayout private(set) var proposedAnswer = UILabel()
18 |
19 | required init?(coder aDecoder: NSCoder) {
20 | NSCoder.fatalErrorNotImplemented()
21 | }
22 |
23 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
24 | super.init(style: style, reuseIdentifier: reuseIdentifier)
25 | [verb, tensePersonNumber, correctAnswer, proposedAnswer].forEach {
26 | $0.textColor = Colors.yellow
27 | $0.font = Fonts.smallCell
28 | addSubview($0)
29 | }
30 | verb.font = Fonts.regularCell
31 | backgroundColor = Colors.black
32 | selectionStyle = .none
33 |
34 | NSLayoutConstraint.activate([
35 | verb.topAnchor.constraint(equalTo: topAnchor, constant: 4.0),
36 | verb.centerXAnchor.constraint(equalTo: centerXAnchor),
37 | tensePersonNumber.topAnchor.constraint(equalTo: verb.bottomAnchor, constant: 4.0),
38 | tensePersonNumber.centerXAnchor.constraint(equalTo: centerXAnchor),
39 | correctAnswer.topAnchor.constraint(equalTo: tensePersonNumber.bottomAnchor, constant: 4.0),
40 | correctAnswer.centerXAnchor.constraint(equalTo: centerXAnchor),
41 | proposedAnswer.topAnchor.constraint(equalTo: correctAnswer.bottomAnchor, constant: 4.0),
42 | proposedAnswer.centerXAnchor.constraint(equalTo: centerXAnchor)
43 | ])
44 | }
45 |
46 | func configure(verb: String, tense: Tense, personNumber: PersonNumber, correctAnswer: String, proposedAnswer: String) {
47 | self.verb.text = verb.lowercased()
48 | tensePersonNumber.text = "\(tense.displayName), \(personNumber.shortDisplayName)"
49 | self.correctAnswer.attributedText = correctAnswer.conjugatedString
50 | self.proposedAnswer.text = proposedAnswer.lowercased()
51 | if correctAnswer.lowercased() != proposedAnswer.lowercased() {
52 | self.proposedAnswer.textColor = Colors.blue
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/ConjugarTests/Utils/ReviewPrompterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewPrompterTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 11/21/18.
6 | // Copyright © 2018 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class ReviewPrompterTests: XCTestCase {
13 | func testPromptableActionHappened() {
14 | let now = Date()
15 | let smallAmountOfTime: TimeInterval = 5.0
16 | let recentPromptDate = now.addingTimeInterval(-1.0 * smallAmountOfTime)
17 |
18 | let formatter = DateFormatter()
19 | let format = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
20 | formatter.dateFormat = format
21 |
22 | var settingsDictionary1: [String: String] = [:]
23 | settingsDictionary1[Settings.lastReviewPromptDateKey] = formatter.string(from: recentPromptDate)
24 | let settings1 = Settings(getterSetter: DictionaryGetterSetter(dictionary: settingsDictionary1))
25 | var didRequestReview = false
26 | let prompter1 = ReviewPrompter(settings: settings1, now: now, requestReview: { didRequestReview = true })
27 |
28 | prompter1.promptableActionHappened()
29 | XCTAssertFalse(didRequestReview)
30 |
31 | settings1.promptActionCount = ReviewPrompter.promptModulo - 1
32 | XCTAssertFalse(didRequestReview)
33 |
34 | let longAgoDate = recentPromptDate.addingTimeInterval(-1.0 * ReviewPrompter.promptInterval)
35 | settings1.lastReviewPromptDate = longAgoDate
36 | settings1.promptActionCount = ReviewPrompter.promptModulo - 2
37 | prompter1.promptableActionHappened()
38 | XCTAssertFalse(didRequestReview)
39 |
40 | settings1.promptActionCount = ReviewPrompter.promptModulo - 1
41 | prompter1.promptableActionHappened()
42 | XCTAssert(didRequestReview)
43 |
44 | var settingsDictionary2: [String: String] = [:]
45 | settingsDictionary2[Settings.promptActionCountKey] = "\(ReviewPrompter.promptModulo - 1)"
46 | let settings2 = Settings(getterSetter: DictionaryGetterSetter(dictionary: settingsDictionary2))
47 | let prompter2 = ReviewPrompter(settings: settings2, now: longAgoDate, requestReview: { didRequestReview = true })
48 |
49 | didRequestReview = false
50 | prompter2.promptableActionHappened()
51 | XCTAssert(didRequestReview)
52 |
53 | didRequestReview = false
54 | prompter2.promptableActionHappened()
55 | XCTAssertFalse(didRequestReview)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/ConjugarTests/Analytics/AnalyticsServiceableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsServiceableTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 4/25/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class AnalyticsServiceableTests: XCTestCase {
13 | private let nilServiceMessage = "TestAnalyticsService was nil."
14 | private let nilAnalyticsMessage = "analytics array was nil."
15 |
16 | func testRecordEvent() {
17 | var analytics: [String] = []
18 | let service = TestAnalyticsService(fire: { event in
19 | analytics.append(event)
20 | })
21 |
22 | let 🥥 = "🥥"
23 | XCTAssertFalse(analytics.contains(🥥))
24 | service.recordEvent(🥥)
25 | XCTAssert(analytics.contains(🥥))
26 | }
27 |
28 | func testRecordVisitation() {
29 | var analytics: [String] = []
30 | let service = TestAnalyticsService(fire: { event in
31 | analytics.append(event)
32 | })
33 |
34 | let pizzaViewController = "PizzaViewController"
35 | XCTAssertFalse(analytics.contains("\(service.visited) \(pizzaViewController)"))
36 | service.recordVisitation(viewController: pizzaViewController)
37 | XCTAssert(analytics.contains("\(service.visited) \(service.viewContröller): \(pizzaViewController) "))
38 | }
39 |
40 | func testRecordQuizStart() {
41 | var analytics: [String] = []
42 | let service = TestAnalyticsService(fire: { event in
43 | analytics.append(event)
44 | })
45 |
46 | XCTAssertFalse(analytics.contains(service.quizStart))
47 | service.recordQuizStart()
48 | XCTAssert(analytics.contains(service.quizStart))
49 | }
50 |
51 | func testRecordQuizCompletion() {
52 | var analytics: [String] = []
53 | let service = TestAnalyticsService(fire: { event in
54 | analytics.append(event)
55 | })
56 |
57 | let score = 42
58 | XCTAssertFalse(analytics.contains("\(service.quizCompletion) \(service.scöre): \(score) "))
59 | service.recordQuizCompletion(score: score)
60 | XCTAssert(analytics.contains("\(service.quizCompletion) \(service.scöre): \(score) "))
61 | }
62 |
63 | func testRecordGameCenterAuth() {
64 | var analytics: [String] = []
65 | let service = TestAnalyticsService(fire: { event in
66 | analytics.append(event)
67 | })
68 |
69 | XCTAssertFalse(analytics.contains("\(service.gameCenterAuth)"))
70 | service.recordGameCenterAuth()
71 | XCTAssert(analytics.contains("\(service.gameCenterAuth)"))
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Conjugar/RatingsFetcher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RatingsFetcher.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 3/1/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct RatingsFetcher {
12 | static let iTunesID = "1236500467"
13 | static let errorMessage = "Fetching failed."
14 |
15 | private static let urlInitializationMessage = " URL could not be initializaed."
16 |
17 | static var iTunesURL: URL {
18 | guard let iTunesURL = URL(string: "https://itunes.apple.com/lookup?id=\(iTunesID)") else {
19 | fatalError("iTunes" + urlInitializationMessage)
20 | }
21 | return iTunesURL
22 | }
23 |
24 | static var reviewURL: URL {
25 | guard let reviewURL = URL(string: "https://itunes.apple.com/app/conjugar/id\(iTunesID)?action=write-review") else {
26 | fatalError("Rate/review" + urlInitializationMessage)
27 | }
28 | return reviewURL
29 | }
30 |
31 | static func fetchRatingsDescription(completion: @escaping (String) -> ()) {
32 | let request = URLRequest(url: RatingsFetcher.iTunesURL)
33 |
34 | let task = Current.session.dataTask(with: request) { (responseData, _, error) in
35 | if error != nil {
36 | completion(errorMessage)
37 | return
38 | } else if let responseData = responseData {
39 | guard
40 | let json = try? JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any],
41 | let results = json["results"] as? [[String: Any]],
42 | results.count == 1
43 | else {
44 | completion(errorMessage)
45 | return
46 | }
47 |
48 | let ratingsCount = (results[0])["userRatingCountForCurrentVersion"] as? Int ?? 0
49 |
50 | let description: String
51 | let exhortation = " ¡Sé la primera o el primero!"
52 |
53 | switch ratingsCount {
54 | case 0:
55 | description = Localizations.Settings.noRating + exhortation
56 | case 1:
57 | description = Localizations.Settings.oneRating + " " + Localizations.Settings.addYours
58 | default:
59 | description = String(format: Localizations.Settings.multipleRatings, ratingsCount) + " " + Localizations.Settings.addYours
60 | }
61 | completion(description)
62 | }
63 | }
64 |
65 | task.resume()
66 | }
67 |
68 | static func stubData(ratingsCount: Int) -> Data {
69 | return Data("{ \"resultCount\":1, \"results\": [ { \"userRatingCountForCurrentVersion\": \(ratingsCount) } ] }".utf8)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Conjugar/CommunViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommunViewModel.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 12/15/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct CommunViewModel {
12 | private(set) var title = ""
13 | private(set) var content = ""
14 | private(set) var image = UIImage()
15 | private(set) var imageLabel = ""
16 | private(set) var okayTitle = ""
17 | private(set) var shouldShowOkay = false
18 | private(set) var cancelTitle = ""
19 | private(set) var shouldShowCancel = false
20 | private(set) var actionTitle = ""
21 | private(set) var shouldShowAction = false
22 | private(set) var action: () -> () = {}
23 | let identifier: Int
24 |
25 | init(commun: Commun) {
26 | identifier = commun.identifier
27 | configure(commun: commun)
28 | }
29 |
30 | private mutating func configure(commun: Commun) {
31 | title = stringFromDict(commun.title)
32 | content = stringFromDict(commun.content)
33 | image = commun.image
34 | imageLabel = stringFromDict(commun.imageLabel)
35 |
36 | switch commun.type {
37 | case .information(okayTitle: let okayTitle):
38 | self.okayTitle = stringFromDict(okayTitle)
39 | shouldShowOkay = true
40 | case .newVersion(okayTitle: let okayTitle, actionTitle: let actionTitle, cancelTitle: let cancelTitle, action: let action, alreadyUpdated: let alreadyUpdated):
41 | self.actionTitle = stringFromDict(actionTitle)
42 | self.cancelTitle = stringFromDict(cancelTitle)
43 | self.okayTitle = stringFromDict(okayTitle)
44 | shouldShowCancel = !alreadyUpdated
45 | shouldShowAction = !alreadyUpdated
46 | shouldShowOkay = alreadyUpdated
47 | self.action = action
48 | case .email(actionTitle: let actionTitle, cancelTitle: let cancelTitle, let action):
49 | self.actionTitle = stringFromDict(actionTitle)
50 | self.cancelTitle = stringFromDict(cancelTitle)
51 | shouldShowCancel = true
52 | shouldShowAction = true
53 | self.action = action
54 | case .website(actionTitle: let actionTitle, cancelTitle: let cancelTitle, action: let action):
55 | self.actionTitle = stringFromDict(actionTitle)
56 | self.cancelTitle = stringFromDict(cancelTitle)
57 | shouldShowCancel = true
58 | shouldShowAction = true
59 | self.action = action
60 | }
61 | }
62 |
63 | private func stringFromDict(_ dict: [String: String]) -> String {
64 | dict[Current.locale.languageCode] ?? dict[Current.locale.defaultLanguageCode] ?? ""
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Conjugar/BrowseInfoUIV.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BrowseInfoUIV.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/30/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class BrowseInfoUIV: UIView {
12 | @UsesAutoLayout
13 | var table: UITableView = {
14 | let tableView = UITableView()
15 | tableView.backgroundColor = Colors.black
16 | return tableView
17 | }()
18 |
19 | @UsesAutoLayout
20 | var difficultyControl: UISegmentedControl = {
21 | let control = UISegmentedControl(items: [
22 | Localizations.BrowseInfo.easy,
23 | Localizations.BrowseInfo.easyAndModerate,
24 | Localizations.BrowseInfo.easyModerateAndDifficult
25 | ])
26 | control.selectedSegmentIndex = 0
27 | control.yellowfyText()
28 | return control
29 | }()
30 |
31 | @UsesAutoLayout
32 | private var difficultyLabel: UILabel = {
33 | let label = UILabel()
34 | label.text = Localizations.BrowseInfo.filter
35 | label.textAlignment = .center
36 | label.font = Fonts.smallBody
37 | label.textColor = Colors.yellow
38 | return label
39 | }()
40 |
41 | required init(coder aDecoder: NSCoder) {
42 | NSCoder.fatalErrorNotImplemented()
43 | }
44 |
45 | override init(frame: CGRect) {
46 | super.init(frame: frame)
47 | [table, difficultyLabel, difficultyControl].forEach {
48 | addSubview($0)
49 | }
50 |
51 | NSLayoutConstraint.activate([
52 | table.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
53 | table.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
54 | table.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
55 | table.bottomAnchor.constraint(equalTo: difficultyControl.topAnchor, constant: Layout.defaultSpacing * -1.0),
56 |
57 | difficultyControl.centerXAnchor.constraint(equalTo: centerXAnchor),
58 | difficultyControl.bottomAnchor.constraint(equalTo: difficultyLabel.topAnchor, constant: Layout.defaultSpacing * -1.0),
59 |
60 | difficultyLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
61 | difficultyLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing)
62 | ])
63 | }
64 |
65 | func setupTable(dataSource: UITableViewDataSource, delegate: UITableViewDelegate) {
66 | table.dataSource = dataSource
67 | table.delegate = delegate
68 | table.register(InfoCell.self, forCellReuseIdentifier: InfoCell.identifier)
69 | }
70 |
71 | func reloadTableData() {
72 | table.reloadData()
73 | table.setContentOffset(CGPoint.zero, animated: false)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Conjugar/GameCenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GameCenter.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 6/26/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import GameKit
10 |
11 | class GameCenter: NSObject, GameCenterable, GKGameCenterControllerDelegate {
12 | static let shared = GameCenter()
13 | var isAuthenticated = false
14 | private let localPlayer = GKLocalPlayer.local
15 | private var leaderboardIdentifier = ""
16 | private var onViewController: UIViewController?
17 |
18 | private override init() {}
19 |
20 | func authenticate(onViewController: UIViewController, completion: ((Bool) -> Void)? = nil) {
21 | self.onViewController = onViewController
22 |
23 | localPlayer.authenticateHandler = { viewController, _ in
24 | if let viewController = viewController {
25 | onViewController.present(viewController, animated: true, completion: nil)
26 | } else if self.localPlayer.isAuthenticated {
27 | // print("AUTHENTICATED displayName: \(self.localPlayer.displayName) alias: \(self.localPlayer.alias) playerID: \(self.localPlayer.playerID)")
28 | Current.analytics.recordGameCenterAuth()
29 | self.isAuthenticated = true
30 | SoundPlayer.playRandomApplause()
31 | self.localPlayer.loadDefaultLeaderboardIdentifier { identifier, _ in
32 | self.leaderboardIdentifier = identifier ?? "ERROR"
33 | // print("identifier: \(self.leaderboardIdentifier)")
34 | }
35 | completion?(true)
36 | } else {
37 | SoundPlayer.play(.sadTrombone)
38 | UIAlertController.showMessage(Localizations.gameCenterFailure, title: "😰", okTitle: Localizations.gotIt, onViewController: onViewController)
39 | self.isAuthenticated = false
40 | completion?(false)
41 | }
42 | }
43 | }
44 |
45 | func reportScore(_ score: Int) {
46 | guard isAuthenticated else {
47 | return
48 | }
49 |
50 | GKLeaderboard.submitScore(score, context: 0, player: localPlayer, leaderboardIDs: [leaderboardIdentifier], completionHandler: { _ in })
51 | }
52 |
53 | func showLeaderboard() {
54 | guard isAuthenticated else {
55 | return
56 | }
57 | let gcViewController = GKGameCenterViewController(leaderboardID: leaderboardIdentifier, playerScope: .global, timeScope: .allTime)
58 | gcViewController.gameCenterDelegate = self
59 | onViewController?.present(gcViewController, animated: true, completion: nil)
60 | }
61 |
62 | func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
63 | gameCenterViewController.dismiss(animated: true, completion: nil)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Conjugar/BrowseVerbsVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BrowseVerbsVC.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 3/31/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class BrowseVerbsVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
12 | static let englishTitle = "Browse"
13 |
14 | private var allVerbs: [String] = []
15 | private var regularVerbs: [String] = []
16 | private var irregularVerbs: [String] = []
17 |
18 | private var currentVerbs: [String] {
19 | switch browseVerbsView.filterControl.selectedSegmentIndex {
20 | case 0:
21 | return irregularVerbs
22 | case 1:
23 | return regularVerbs
24 | case 2:
25 | return allVerbs
26 | default:
27 | fatalError("Invalid verb-filter index.")
28 | }
29 | }
30 |
31 | var browseVerbsView: BrowseVerbsUIV {
32 | if let castedView = view as? BrowseVerbsUIV {
33 | return castedView
34 | } else {
35 | fatalError(fatalCastMessage(view: BrowseVerbsUIV.self))
36 | }
37 | }
38 |
39 | override func loadView() {
40 | let browseVerbsView = BrowseVerbsUIV(frame: UIScreen.main.bounds)
41 | browseVerbsView.setupTable(dataSource: self, delegate: self)
42 | browseVerbsView.filterControl.addTarget(self, action: #selector(BrowseVerbsVC.valueChanged(_:)), for: .valueChanged)
43 | allVerbs = Conjugator.shared.allVerbs
44 | regularVerbs = Conjugator.shared.regularVerbs
45 | irregularVerbs = Conjugator.shared.irregularVerbs
46 | navigationItem.titleView = UILabel.titleLabel(title: Localizations.BrowseVerbs.localizedTitle)
47 | view = browseVerbsView
48 | Current.reviewPrompter.promptableActionHappened()
49 | }
50 |
51 | override func viewWillAppear(_ animated: Bool) {
52 | super.viewWillAppear(animated)
53 | browseVerbsView.isHidden = false
54 | Current.analytics.recordVisitation(viewController: "\(BrowseVerbsVC.self)")
55 | }
56 |
57 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
58 | return currentVerbs.count
59 | }
60 |
61 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
62 | guard let cell = tableView.dequeueReusableCell(withIdentifier: VerbCell.identifier) as? VerbCell else {
63 | fatalError("Could not dequeue \(VerbCell.self).")
64 | }
65 | cell.configure(verb: currentVerbs[indexPath.row])
66 | return cell
67 | }
68 |
69 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
70 | tableView.deselectRow(at: indexPath, animated: false)
71 | let verbVC = VerbVC(verb: currentVerbs[indexPath.row])
72 | browseVerbsView.isHidden = true
73 | navigationController?.pushViewController(verbVC, animated: true)
74 | }
75 |
76 | @objc func valueChanged(_ sender: UISegmentedControl) {
77 | browseVerbsView.reloadTableData()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/ConjugarTests/Models/QuizTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuizTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 12/3/18.
6 | // Copyright © 2018 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | // swiftlint:disable private_over_fileprivate
13 | fileprivate let difficultSpain = 750
14 | // swiftlint:enable private_over_fileprivate
15 |
16 | class QuizTests: XCTestCase {
17 | private let testGameCenter = TestGameCenter()
18 |
19 | func testQuiz() {
20 | let spain = Region.spain.rawValue
21 | let latinAmerica = Region.latinAmerica.rawValue
22 | let difficult = Difficulty.difficult.rawValue
23 | let moderate = Difficulty.moderate.rawValue
24 | let easy = Difficulty.easy.rawValue
25 |
26 | let difficultLatinAmerica = 624
27 | let moderateSpain = 500
28 | let moderateLatinAmerica = 416
29 | let easySpain = 250
30 | let easyLatinAmerica = 208
31 |
32 | [(spain, difficult, difficultSpain),
33 | (latinAmerica, difficult, difficultLatinAmerica),
34 | (spain, moderate, moderateSpain),
35 | (latinAmerica, moderate, moderateLatinAmerica),
36 | (spain, easy, easySpain),
37 | (latinAmerica, easy, easyLatinAmerica)
38 | ].forEach { region, difficulty, maxScore in
39 | let settings = Settings(getterSetter: DictionaryGetterSetter(dictionary: [Settings.difficultyKey: difficulty, Settings.regionKey: region]))
40 | let quiz = Quiz(settings: settings, gameCenter: testGameCenter, shouldShuffle: true)
41 | _ = TestQuizDelegate(quiz: quiz, onFinish: { score in
42 | XCTAssertEqual(score, maxScore)
43 | })
44 | }
45 | }
46 | }
47 |
48 | class TestQuizDelegate: QuizDelegate {
49 | let quiz: Quiz
50 | let onFinish: (Int) -> ()
51 | private var score = 0
52 |
53 | init(quiz: Quiz, onFinish: @escaping (Int) -> ()) {
54 | self.quiz = quiz
55 | self.onFinish = onFinish
56 | quiz.delegate = self
57 | quiz.start()
58 | }
59 |
60 | func questionDidChange(verb: String, tense: Tense, personNumber: PersonNumber) {
61 | let conjugationResult = Conjugator.shared.conjugate(infinitive: verb, tense: tense, personNumber: personNumber)
62 | switch conjugationResult {
63 | case let .success(value):
64 | _ = quiz.process(proposedAnswer: value)
65 | default:
66 | fatalError("Conjugation failed during unit test.")
67 | }
68 | }
69 |
70 | func quizDidFinish() {
71 | onFinish(quiz.score)
72 | }
73 |
74 | func scoreDidChange(newScore: Int) {
75 | score = newScore
76 | XCTAssert(score >= 0 && score <= difficultSpain)
77 | }
78 |
79 | func timeDidChange(newTime: Int) {
80 | // Note: The time is always 0, so I'm not going to bother testing it.
81 | // I could slow down the test so it takes longer than 0 second, but
82 | // that would be contrary to the quickness goal of unit tests.
83 | }
84 |
85 | func progressDidChange(current: Int, total: Int) {
86 | let questionCount = 50
87 | XCTAssert(current >= 0 && current < questionCount)
88 | XCTAssertEqual(total, questionCount)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/CommunVCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommunVCTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 12/21/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class CommunVCTests: XCTestCase {
13 | func testTaps() {
14 | let settings = Settings(getterSetter: DictionaryGetterSetter())
15 | let gameCenter = TestGameCenter(isAuthenticated: false)
16 | let analytics = TestAnalyticsService()
17 | let quiz = Quiz(settings: settings, gameCenter: gameCenter, shouldShuffle: false)
18 | let communGetter = StubCommunGetter()
19 |
20 | Current = World(
21 | analytics: analytics,
22 | reviewPrompter: TestReviewPrompter(),
23 | gameCenter: gameCenter,
24 | settings: settings,
25 | quiz: quiz,
26 | session: URLSession.stubSession(ratingsCount: 0),
27 | communGetter: communGetter,
28 | locale: StubLocale(languageCode: "en", regionCode: "US")
29 | )
30 |
31 | let window = UIWindow(frame: UIScreen.main.bounds)
32 | let rootVC = UIViewController()
33 | window.rootViewController = rootVC
34 | window.makeKeyAndVisible()
35 | var cvc = CommunVC(commun: communGetter.information)
36 |
37 | rootVC.present(cvc, animated: false)
38 | XCTAssert(rootVC.presentedViewController is CommunVC)
39 | var exp = expectation(description: "tap happened")
40 | cvc.unitTestCompletion = {
41 | XCTAssertNil(rootVC.presentedViewController)
42 | exp.fulfill()
43 | }
44 | cvc.tapClose()
45 | let timeout: TimeInterval = 1.0
46 | waitForExpectations(timeout: timeout)
47 |
48 | rootVC.present(cvc, animated: false)
49 | XCTAssert(rootVC.presentedViewController is CommunVC)
50 | exp = expectation(description: "tap happened")
51 | cvc.unitTestCompletion = {
52 | XCTAssertNil(rootVC.presentedViewController)
53 | exp.fulfill()
54 | }
55 | cvc.tapOkay()
56 | waitForExpectations(timeout: timeout)
57 |
58 | var didTapAction = false
59 | let ctaType = Commun.CommunType.website(actionTitle: ["en": "🐬"], cancelTitle: ["en": "🐉"], action: { didTapAction = true })
60 | let ctaCommun = Commun(title: ["en": "🥥"], image: UIImage(), imageLabel: ["en": "🍕"], content: ["en": "🐋"], type: ctaType, identifier: 0)
61 | cvc = CommunVC(commun: ctaCommun)
62 |
63 | rootVC.present(cvc, animated: false)
64 | XCTAssert(rootVC.presentedViewController is CommunVC)
65 | exp = expectation(description: "tap happened")
66 | cvc.unitTestCompletion = {
67 | XCTAssertNil(rootVC.presentedViewController)
68 | exp.fulfill()
69 | }
70 | cvc.tapCancel()
71 | waitForExpectations(timeout: timeout)
72 |
73 | rootVC.present(cvc, animated: false)
74 | XCTAssert(rootVC.presentedViewController is CommunVC)
75 | exp = expectation(description: "tap happened")
76 | cvc.unitTestCompletion = {
77 | XCTAssertNil(rootVC.presentedViewController)
78 | XCTAssert(didTapAction)
79 | exp.fulfill()
80 | }
81 | cvc.tapAction()
82 | waitForExpectations(timeout: timeout)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/ConjugarTests/Controllers/QuizVCTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuizVCTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/24/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class QuizVCTests: XCTestCase {
13 | func testQuizVC() {
14 | var analytic = ""
15 | let settings = Settings(getterSetter: DictionaryGetterSetter())
16 | settings.userRejectedGameCenter = true
17 | let gameCenter = TestGameCenter(isAuthenticated: false)
18 | let analytics = TestAnalyticsService(fire: { fired in analytic = fired })
19 | let quiz = Quiz(settings: settings, gameCenter: gameCenter, shouldShuffle: false)
20 | let fakeRatingsCount = 42
21 |
22 | Current = World(
23 | analytics: analytics,
24 | reviewPrompter: TestReviewPrompter(),
25 | gameCenter: gameCenter,
26 | settings: settings,
27 | quiz: quiz,
28 | session: URLSession.stubSession(ratingsCount: fakeRatingsCount),
29 | communGetter: StubCommunGetter(),
30 | locale: StubLocale(languageCode: "en", regionCode: "US")
31 | )
32 |
33 | let qvc = QuizVC()
34 |
35 | XCTAssertNotNil(qvc)
36 | XCTAssertNotNil(qvc.quizView)
37 | qvc.viewWillAppear(true)
38 | XCTAssertEqual(analytic, "visited viewController: \(QuizVC.self) ")
39 | let quizView = qvc.quizView
40 | XCTAssertEqual(quizView.startRestartButton.titleLabel?.text, "Start")
41 | qvc.startRestart()
42 | XCTAssertEqual(quizView.startRestartButton.titleLabel?.text, "Restart")
43 | qvc.viewWillAppear(true)
44 | XCTAssertEqual(quizView.startRestartButton.titleLabel?.text, "Restart")
45 |
46 | XCTAssertEqual(quizView.score.text, "0")
47 | XCTAssertEqual(quizView.progress.text, "1 / 50")
48 | quizView.conjugationField.text = "caminas"
49 | XCTAssert(qvc.textFieldShouldReturn(quizView.conjugationField))
50 | [quizView.lastLabel, quizView.last, quizView.correctLabel, quizView.correct].forEach {
51 | XCTAssert($0.isHidden)
52 | }
53 |
54 | let wrongAnswer = "🥥"
55 | let correctAnswer = "anda"
56 | XCTAssertEqual(quizView.score.text, "10")
57 | XCTAssertEqual(quizView.progress.text, "2 / 50")
58 | quizView.conjugationField.text = wrongAnswer
59 | XCTAssert(qvc.textFieldShouldReturn(quizView.conjugationField))
60 | [quizView.lastLabel, quizView.last, quizView.correctLabel, quizView.correct].forEach {
61 | XCTAssertFalse($0.isHidden)
62 | }
63 | XCTAssertEqual(quizView.last.text, wrongAnswer)
64 | XCTAssertEqual(quizView.correct.text, correctAnswer)
65 |
66 | let remainingQuestionCount = 48
67 | for _ in 0 ..< remainingQuestionCount {
68 | quizView.conjugationField.text = wrongAnswer
69 | XCTAssert(qvc.textFieldShouldReturn(quizView.conjugationField))
70 | }
71 | let expectedCompletionAnalytic = "quizCompletion score: 4 "
72 | XCTAssertEqual(analytic, expectedCompletionAnalytic)
73 |
74 | XCTAssert(quizView.score.isHidden)
75 | qvc.startRestart()
76 | XCTAssertFalse(quizView.score.isHidden)
77 | qvc.quit()
78 | XCTAssert(quizView.score.isHidden)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | ### Introduction
4 |
5 | **Conjugar** is an iPhone™ app for learning Spanish verb conjugations. **Conjugar** conjugates most Spanish verbs, regular and irregular, in **all** Spanish verb tenses. There is a quiz mode with three difficulty levels. Results from quizzes are available in Game Center™. On a pedagogical note, **Conjugar** contains descriptions of the tenses.
6 |
7 | **Conjugar** uses dependency injection (DI) and programmatic layout (PL). Thus, if you are curious about how to implement DI or PL, **Conjugar** may be instructive. I have written tutorials on [DI](https://racecondition.software/blog/dependency-injection/) and [PL](https://racecondition.software/blog/programmatic-layout/).
8 |
9 | ### Installation
10 |
11 | **Conjugar** is available for free download in the iOS App Store™. Tap the logo below to install.
12 |
13 | [](https://itunes.apple.com/us/app/conjugar/id1236500467?mt=8)
14 |
15 | Alternatively, you can clone this repo and build, using Xcode™, **Conjugar** yourself.
16 |
17 | **Conjugar** is currently using AWS Pinpoint analytics. The two relevant frameworks are in source control, but the configuration files and folder, in particular `awsconfiguration.json`, `.amplifyrc`, and `amplify`, respectively, are excluded from source control by the `.gitignore` file. For instructions on Pinpoint configuration, see this excellent [tutorial](https://itnext.io/integrate-analytics-into-your-ios-swift-applications-with-aws-amplify-20d31fe0a20e).
18 |
19 | If you want to build **Conjugar** without using AWS Pinpoint analytics, you can use the following workaround:
20 |
21 | * Remove AWSFrameworks from the 'Embed Frameworks' build phase.
22 | * Comment out script in the the Pinpoint Hocus Pocus build phase.
23 | * Remove `awsconfiguration.json` from being copied in the Copy Resources build phase.
24 | * Comment out `import AWSPinpoint` and all the contents of the methods in AWSAnalyticsService.swift.
25 |
26 | Please make sure to avoid committing these changes!
27 |
28 | ### License
29 |
30 | If Conjugar is in the App Store, why is the code on GitHub? I created this app to demonstrate programmatic layout for a conference talk, and I wish to provide helpful example code for folks who are curious about programmatic layout. I originally released Conjugar's source code under the MIT License because that license is maximally convenient for would-be users of the programmatic-layout code. This was a mistake. Some dirtbag released a _clone_ of Conjugar on the App Store that differs only in that it has a hideous app icon, that it requests push-notification permission, and that it crashes on launch. I have changed the MIT License to the GNU Affero General Public License in order to impose onerous requirements on would-be cloners of Conjugar.
31 |
32 | ### Screenshots
33 |
34 | 
35 |
36 | 
37 |
38 | 
39 |
40 | 
41 |
42 | 
43 |
44 | 
45 |
46 | 
47 |
--------------------------------------------------------------------------------
/Conjugar/VerbFamilies.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerbFamilies.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 6/10/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | struct VerbFamilies {
10 | static let regularArVerbs = ["hablar", "caminar", "andar", "trabajar", "estudiar", "escuchar", "visitar", "viajar", "enseñar", "llevar", "bailar", "nadar", "cocinar", "charlar", "platicar", "llorar", "esperar", "buscar", "mirar", "pintar", "gastar", "ganar", "comprar", "tocar", "tomar", "sacar", "ayudar", "cantar", "desear", "necesitar", "cortar", "contestar", "dibujar", "clonar", "datar", "distar", "empinar", "encalar", "esposar", "formar", "glosar", "golpear", "grapar", "hibernar", "maltear", "manar", "nublar", "penar", "ponderar", "rasar", "seriar", "trinchar", "procesar", "declarar", "helar", "usar", "regresar", "quedar", "lavar", "limpiar", "amar"]
11 |
12 | static let regularIrVerbs = ["vivir", "existir", "ocurrir", "recibir", "permitir", "partir", "cumplir", "decidir", "subir", "sufrir", "compartir", "consistir", "insistir", "asistir", "discutir", "unir", "coincidir", "distinguir", "definir", "admitir", "acudir", "nutrir", "evadir"]
13 |
14 | static let regularErVerbs = ["comer", "beber", "leer", "aprender", "comprender", "correr", "deber", "vender", "romper", "temer", "reprender", "barrer", "cometer", "poseer", "responder", "prometer", "meter", "someter", "absorber", "emprender", "coser", "ceder", "exceder", "ofender", "esconder", "lamer", "tejer", "esconder"]
15 |
16 | static let allRegularVerbs = VerbFamilies.regularArVerbs + VerbFamilies.regularErVerbs + VerbFamilies.regularIrVerbs
17 |
18 | static let irregularPresenteDeIndicativoVerbs = ["ser", "ir", "dormir", "hacer", "morir", "morder", "oír", "poder", "haber", "sentir", "sentar"]
19 |
20 | static let irregularImperfectivoVerbs = ["ser", "ir", "ver"]
21 |
22 | static let irregularPreteritoVerbs = ["ser", "ir", "dar", "poner", "poder", "estar", "tener", "andar", "saber", "haber", "caber", "hacer", "venir", "querer", "decir", "traer", "conducir"]
23 |
24 | static let irregularRaizFuturaVerbs = ["haber", "saber", "caber", "poder", "querer", "poner", "tener", "venir", "salir", "valer", "decir", "hacer"]
25 |
26 | static let irregularPresenteDeSubjuntivoVerbs = ["ser", "ir", "haber", "saber", "pensar", "perder", "sentir", "dormir", "pedir", "crecer", "conocer", "lucir", "conducir", "huir", "construir", "estar", "dar", "caber", "decir", "hacer", "caer", "oír", "traer", "poner", "salir", "tener", "valer", "venir", "ver", "jugar", "argüir", "elegir", "colegir", "manecer", "anochecer", "cazar", "granizar"]
27 |
28 | static let irregularTuImperativoVerbs = ["ser", "ir", "decir", "hacer", "poner", "salir", "tener", "venir", "componer", "obtener", "medir", "pedir", "oír", "elegir", "colegir"]
29 |
30 | static let irregularVosImperativoVerbs = ["ser", "ir"]
31 |
32 | static let irregularParticipioVerbs = ["abrir", "cubrir", "decir", "escribir", "hacer", "morir", "poner", "resolver", "romper", "ver", "volver", "pudrir"]
33 |
34 | static let irregularGerundioVerbs = ["poder", "sentir", "medir", "dormir", "caer", "leer", "traer", "construir", "huir", "oír", "ir", "tañer", "bullir", "argüir", "elegir", "colegir", "hervir"]
35 |
36 | static let thirdPersonSingularOnlyVerbs = ["amanecer", "anochecer", "llover", "granizar", "nevar", "relampaguear", "tronar"]
37 | }
38 |
--------------------------------------------------------------------------------
/Conjugar/CommunVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommunVC.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 12/13/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class CommunVC: UIViewController {
12 | var communView: CommunUIV {
13 | if let castedView = view as? CommunUIV {
14 | return castedView
15 | } else {
16 | fatalError(fatalCastMessage(view: CommunUIV.self))
17 | }
18 | }
19 |
20 | var viewModel: CommunViewModel?
21 | var unitTestCompletion: (() -> ())?
22 | private let unknownIdentifier = -42
23 |
24 | init(commun: Commun) {
25 | super.init(nibName: nil, bundle: nil)
26 | viewModel = CommunViewModel(commun: commun)
27 | }
28 |
29 | required init?(coder aDecoder: NSCoder) {
30 | NSCoder.fatalErrorNotImplemented()
31 | }
32 |
33 | override func loadView() {
34 | let communView: CommunUIV
35 | communView = CommunUIV(frame: UIScreen.main.bounds)
36 | communView.closeButton.addTarget(self, action: #selector(tapClose), for: .touchUpInside)
37 | communView.okayButton.addTarget(self, action: #selector(tapOkay), for: .touchUpInside)
38 | communView.actionButton.addTarget(self, action: #selector(tapAction), for: .touchUpInside)
39 | communView.cancelButton.addTarget(self, action: #selector(tapCancel), for: .touchUpInside)
40 | view = communView
41 | configureUI()
42 | }
43 |
44 | override func viewWillAppear(_ animated: Bool) {
45 | super.viewWillAppear(animated)
46 | if let identifier = viewModel?.identifier {
47 | Current.analytics.recordCommunVisitation(identifier: identifier)
48 | }
49 | }
50 |
51 | private func configureUI() {
52 | guard let viewModel = viewModel else {
53 | fatalError("\(CommunViewModel.self) not initialized.")
54 | }
55 |
56 | communView.title.text = viewModel.title
57 | communView.content.text = viewModel.content
58 | communView.imageView.image = viewModel.image
59 | communView.imageView.accessibilityLabel = viewModel.imageLabel
60 | communView.okayButton.isHidden = !viewModel.shouldShowOkay
61 | communView.okayButton.setTitle(viewModel.okayTitle, for: .normal)
62 | communView.cancelButton.isHidden = !viewModel.shouldShowCancel
63 | communView.cancelButton.setTitle(viewModel.cancelTitle, for: .normal)
64 | communView.actionButton.isHidden = !viewModel.shouldShowAction
65 | communView.actionButton.setTitle(viewModel.actionTitle, for: .normal)
66 | }
67 |
68 | @objc func tapClose() {
69 | Current.analytics.recordCloseTap(identifier: viewModel?.identifier ?? unknownIdentifier)
70 | dismiss(animated: true, completion: unitTestCompletion)
71 | }
72 |
73 | @objc func tapOkay() {
74 | Current.analytics.recordOkayTap(identifier: viewModel?.identifier ?? unknownIdentifier)
75 | dismiss(animated: true, completion: unitTestCompletion)
76 | }
77 |
78 | @objc func tapAction() {
79 | Current.analytics.recordActionTap(identifier: viewModel?.identifier ?? unknownIdentifier)
80 | SoundPlayer.playRandomApplause()
81 | dismiss(animated: true, completion: { [weak self] in
82 | self?.viewModel?.action()
83 | self?.unitTestCompletion?()
84 | })
85 | }
86 |
87 | @objc func tapCancel() {
88 | Current.analytics.recordCancelTap(identifier: viewModel?.identifier ?? unknownIdentifier)
89 | dismiss(animated: true, completion: unitTestCompletion)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Conjugar/ResultsUIV.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResultsUIV.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 8/7/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ResultsUIV: UIView {
12 | @UsesAutoLayout
13 | var table: UITableView = {
14 | let tableView = UITableView()
15 | tableView.backgroundColor = Colors.black
16 | tableView.rowHeight = 120
17 | return tableView
18 | }()
19 |
20 | @UsesAutoLayout var difficulty = UILabel()
21 | @UsesAutoLayout var region = UILabel()
22 | @UsesAutoLayout var score = UILabel()
23 | @UsesAutoLayout var time = UILabel()
24 | @UsesAutoLayout var scoreLabel = UILabel()
25 | @UsesAutoLayout var timeLabel = UILabel()
26 |
27 | required init(coder aDecoder: NSCoder) {
28 | NSCoder.fatalErrorNotImplemented()
29 | }
30 |
31 | override init(frame: CGRect) {
32 | super.init(frame: frame)
33 | [difficulty, region, score, time, scoreLabel, timeLabel].forEach {
34 | $0.textColor = Colors.yellow
35 | $0.font = Fonts.label
36 | }
37 | [table, difficulty, region, score, time, scoreLabel, timeLabel].forEach {
38 | addSubview($0)
39 | }
40 | [(scoreLabel, Localizations.score + ":"), (timeLabel, Localizations.Results.time + ":")].forEach {
41 | $0.0.text = $0.1
42 | }
43 |
44 | NSLayoutConstraint.activate([
45 | table.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
46 | table.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
47 | table.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
48 | table.bottomAnchor.constraint(equalTo: difficulty.topAnchor, constant: Layout.defaultSpacing * -1.0),
49 |
50 | difficulty.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
51 | difficulty.bottomAnchor.constraint(equalTo: score.topAnchor, constant: Layout.defaultSpacing * -1.0),
52 |
53 | region.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
54 | region.bottomAnchor.constraint(equalTo: time.topAnchor, constant: Layout.defaultSpacing * -1.0),
55 |
56 | scoreLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
57 | scoreLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing),
58 |
59 | timeLabel.trailingAnchor.constraint(equalTo: time.leadingAnchor, constant: Layout.defaultSpacing * -1.0),
60 | timeLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing),
61 |
62 | time.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
63 | time.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing),
64 |
65 | score.leadingAnchor.constraint(equalTo: scoreLabel.trailingAnchor, constant: Layout.defaultSpacing),
66 | score.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing),
67 |
68 | time.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
69 | time.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing)
70 | ])
71 | }
72 |
73 | func setupTable(dataSource: UITableViewDataSource, delegate: UITableViewDelegate) {
74 | table.dataSource = dataSource
75 | table.delegate = delegate
76 | table.register(ResultCell.self, forCellReuseIdentifier: ResultCell.identifier)
77 | }
78 |
79 | func reloadTableData() {
80 | table.reloadData()
81 | table.setContentOffset(CGPoint.zero, animated: false)
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Conjugar/VerbUIV.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerbUIV.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/18/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class VerbUIV: UIView {
12 | @UsesAutoLayout var translation = UILabel()
13 | @UsesAutoLayout var parentOrType = UILabel()
14 | @UsesAutoLayout var participio = UILabel()
15 | @UsesAutoLayout var gerundio = UILabel()
16 | @UsesAutoLayout var raízFutura = UILabel()
17 | @UsesAutoLayout var defectuoso = UILabel()
18 | @UsesAutoLayout private var raízFuturaLabel = UILabel()
19 |
20 | @UsesAutoLayout
21 | var table: UITableView = {
22 | let tableView = UITableView()
23 | tableView.backgroundColor = Colors.black
24 | return tableView
25 | }()
26 |
27 | required init(coder aDecoder: NSCoder) {
28 | NSCoder.fatalErrorNotImplemented()
29 | }
30 |
31 | override init(frame: CGRect) {
32 | super.init(frame: frame)
33 | [translation, parentOrType, participio, gerundio, raízFuturaLabel, raízFutura, defectuoso].forEach {
34 | $0.font = Fonts.label
35 | $0.textColor = Colors.yellow
36 | }
37 | raízFuturaLabel.text = "RF:"
38 |
39 | [translation, participio, gerundio, raízFutura, defectuoso].forEach {
40 | $0.isUserInteractionEnabled = true
41 | }
42 |
43 | [table, translation, parentOrType, participio, gerundio, raízFuturaLabel, raízFutura, defectuoso].forEach {
44 | addSubview($0)
45 | }
46 |
47 | NSLayoutConstraint.activate([
48 | translation.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: Layout.defaultSpacing),
49 | translation.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
50 |
51 | parentOrType.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: Layout.defaultSpacing),
52 | parentOrType.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
53 |
54 | participio.topAnchor.constraint(equalTo: translation.bottomAnchor, constant: Layout.defaultSpacing),
55 | participio.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
56 |
57 | gerundio.topAnchor.constraint(equalTo: parentOrType.bottomAnchor, constant: Layout.defaultSpacing),
58 | gerundio.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
59 |
60 | raízFuturaLabel.topAnchor.constraint(equalTo: participio.bottomAnchor, constant: Layout.defaultSpacing),
61 | raízFuturaLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
62 |
63 | raízFutura.topAnchor.constraint(equalTo: participio.bottomAnchor, constant: Layout.defaultSpacing),
64 | raízFutura.leadingAnchor.constraint(equalTo: raízFuturaLabel.trailingAnchor, constant: Layout.defaultSpacing),
65 |
66 | defectuoso.topAnchor.constraint(equalTo: gerundio.bottomAnchor, constant: Layout.defaultSpacing),
67 | defectuoso.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
68 |
69 | table.topAnchor.constraint(equalTo: raízFuturaLabel.bottomAnchor, constant: Layout.defaultSpacing),
70 | table.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
71 | table.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
72 | table.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -1.0 * Layout.defaultSpacing)
73 | ])
74 | }
75 |
76 | func setupTable(dataSource: UITableViewDataSource, delegate: UITableViewDelegate) {
77 | table.dataSource = dataSource
78 | table.delegate = delegate
79 | table.register(TenseCell.self, forCellReuseIdentifier: TenseCell.identifier)
80 | table.register(ConjugationCell.self, forCellReuseIdentifier: ConjugationCell.identifier)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Conjugar/ConjugationDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConjugationDataSource.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 1/15/18.
6 | // Copyright © 2018 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | enum ConjugationRow {
12 | case tense(Tense)
13 | case conjugation(Tense, PersonNumber, String)
14 | }
15 |
16 | class ConjugationDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
17 | let rowCount: Int
18 | let verb: String
19 | weak var table: UITableView?
20 | var rows: [ConjugationRow] = []
21 |
22 | init(verb: String, table: UITableView, secondSingularBrowse: SecondSingularBrowse) {
23 | self.verb = verb
24 | self.table = table
25 | let tenses = Tense.conjugatedTenses
26 | rowCount = tenses.reduce(0, { $0 + $1.conjugationCount(secondSingularBrowse: secondSingularBrowse) }) + tenses.count
27 | super.init()
28 | tenses.forEach { tense in
29 | self.rows.append(.tense(tense))
30 | if tense.hasYoForm {
31 | let yoResult = Conjugator.shared.conjugate(infinitive: verb, tense: tense, personNumber: .firstSingular)
32 | switch yoResult {
33 | case let .success(value):
34 | self.rows.append(.conjugation(tense, .firstSingular, value))
35 | default:
36 | fatalError("No yo form found for tense \(tense.displayName).")
37 | }
38 | }
39 |
40 | let tuResult = Conjugator.shared.conjugate(infinitive: verb, tense: tense, personNumber: .secondSingularTú)
41 | let tuConjugation: String
42 | switch tuResult {
43 | case let .success(value):
44 | tuConjugation = value
45 | default:
46 | fatalError("No tú form found for tense \(tense.displayName).")
47 | }
48 | let vosResult = Conjugator.shared.conjugate(infinitive: verb, tense: tense, personNumber: .secondSingularVos)
49 | let vosConjugation: String
50 | switch vosResult {
51 | case let .success(value):
52 | vosConjugation = value
53 | default:
54 | fatalError("No vos form found for tense \(tense.displayName).")
55 | }
56 | switch secondSingularBrowse {
57 | case .tu:
58 | self.rows.append(.conjugation(tense, .secondSingularTú, tuConjugation))
59 | case .vos:
60 | self.rows.append(.conjugation(tense, .secondSingularVos, vosConjugation))
61 | case .both:
62 | self.rows.append(.conjugation(tense, .secondSingularTú, tuConjugation))
63 | self.rows.append(.conjugation(tense, .secondSingularVos, vosConjugation))
64 | }
65 | [PersonNumber.thirdSingular, .firstPlural, .secondPlural, .thirdPlural].forEach { personNumber in
66 | let result = Conjugator.shared.conjugate(infinitive: verb, tense: tense, personNumber: personNumber)
67 | switch result {
68 | case let .success(value):
69 | self.rows.append(.conjugation(tense, personNumber, value))
70 | default:
71 | fatalError("No \(personNumber.pronoun) form found.")
72 | }
73 | }
74 | }
75 | }
76 |
77 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
78 | return rowCount
79 | }
80 |
81 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
82 | switch rows[indexPath.row] {
83 | case .tense(let tense):
84 | guard let cell = table?.dequeueReusableCell(withIdentifier: TenseCell.identifier) as? TenseCell else {
85 | fatalError("Failed to dequeue cell for tense \(tense).")
86 | }
87 | cell.configure(tense: tense.titleCaseName)
88 | return cell
89 | case .conjugation(let tense, let personNumber, let conjugation):
90 | guard let cell = table?.dequeueReusableCell(withIdentifier: ConjugationCell.identifier) as? ConjugationCell else {
91 | fatalError("Failed to dequeue cell for tense \(tense), personNumber \(personNumber), and conjugation \(conjugation).")
92 | }
93 | cell.configure(tense: tense, personNumber: personNumber, conjugation: conjugation)
94 | return cell
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Conjugar/AnalyticsServiceable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnalyticsServiceable.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 11/24/18.
6 | // Copyright © 2018 Josh Adams. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | protocol AnalyticsServiceable {
13 | func recordEvent(_ eventName: String, parameters: [String: String]?, metrics: [String: Double]?)
14 | func recordEvent(_ eventName: String)
15 | func recordVisitation(viewController: String)
16 | func recordCommunVisitation(identifier: Int)
17 | func recordOkayTap(identifier: Int)
18 | func recordActionTap(identifier: Int)
19 | func recordCancelTap(identifier: Int)
20 | func recordQuizStart()
21 | func recordQuizCompletion(score: Int)
22 | func recordGameCenterAuth()
23 | func recordBecameActive()
24 |
25 | var visited: String { get }
26 | var viewContröller: String { get }
27 | var quizStart: String { get }
28 | var quizCompletion: String { get }
29 | var scöre: String { get }
30 | var gameCenterAuth: String { get }
31 | }
32 |
33 | extension AnalyticsServiceable {
34 | func recordEvent(_ eventName: String) {
35 | recordEvent(eventName, parameters: nil, metrics: nil)
36 | }
37 |
38 | func recordVisitation(viewController: String) {
39 | recordEvent(visited, parameters: [viewContröller: "\(viewController)"], metrics: nil)
40 | }
41 |
42 | func recordCommunVisitation(identifier: Int) {
43 | recordVisitation(viewController: "\(CommunVC.self) \(identifier)")
44 | }
45 |
46 | func recordOkayTap(identifier: Int) {
47 | recordEvent(okayTapped, parameters: [identifīer: "\(identifier)"], metrics: nil)
48 | }
49 |
50 | func recordActionTap(identifier: Int) {
51 | recordEvent(actionTapped, parameters: [identifīer: "\(identifier)"], metrics: nil)
52 | }
53 |
54 | func recordCancelTap(identifier: Int) {
55 | recordEvent(cancelTapped, parameters: [identifīer: "\(identifier)"], metrics: nil)
56 | }
57 |
58 | func recordCloseTap(identifier: Int) {
59 | recordEvent(closeTapped, parameters: [identifīer: "\(identifier)"], metrics: nil)
60 | }
61 |
62 | func recordQuizStart() {
63 | recordEvent(quizStart)
64 | }
65 |
66 | func recordQuizCompletion(score: Int) {
67 | recordEvent(quizCompletion, parameters: [scöre: "\(score)"], metrics: nil)
68 | }
69 |
70 | func recordQuizQuit(currentQuestionIndex: Int, score: Int) {
71 | recordEvent(quizQuit, parameters: [cürrentQuestionIndex: "\(currentQuestionIndex)", scöre: "\(score)"], metrics: nil)
72 | }
73 |
74 | func recordGameCenterAuth() {
75 | recordEvent(gameCenterAuth)
76 | }
77 |
78 | func recordBecameActive() {
79 | let becameActive = "becameActive"
80 | let modelKey = "model"
81 | let localeKey = "locale"
82 |
83 | let modelName = UIDevice.current.modelName
84 | let locale = Current.locale.locale
85 |
86 | recordEvent(becameActive, parameters: [modelKey: modelName, localeKey: locale], metrics: nil)
87 | }
88 |
89 | var visited: String {
90 | return "visited"
91 | }
92 |
93 | var viewContröller: String {
94 | return "viewController"
95 | }
96 |
97 | var okayTapped: String {
98 | return "okayTapped"
99 | }
100 |
101 | var actionTapped: String {
102 | return "actionTapped"
103 | }
104 |
105 | var cancelTapped: String {
106 | return "cancelTapped"
107 | }
108 |
109 | var closeTapped: String {
110 | return "closeTapped"
111 | }
112 |
113 | var identifīer: String {
114 | return "identifier"
115 | }
116 |
117 | var quizStart: String {
118 | return "quizStart"
119 | }
120 |
121 | var quizQuit: String {
122 | return "quizQuit"
123 | }
124 |
125 | var quizCompletion: String {
126 | return "quizCompletion"
127 | }
128 |
129 | var scöre: String {
130 | return "score"
131 | }
132 |
133 | var cürrentQuestionIndex: String {
134 | return "currentQuestionIndex"
135 | }
136 |
137 | var gameCenterAuth: String {
138 | return "gameCenterAuth"
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/ConjugarTests/Models/TenseTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TenseTests.swift
3 | // ConjugarTests
4 | //
5 | // Created by Joshua Adams on 5/14/19.
6 | // Copyright © 2019 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Conjugar
11 |
12 | class TenseTests: XCTestCase {
13 | func testDisplayNames() {
14 | [(Tense.infinitivo, "infinitivo"), (.translation, "translation"), (.gerundio, "gerundio"), (.participio, "participio"), (.raízFutura, "raíz futura"), (.imperativoPositivo, "imperativo positivo"), (.imperativoNegativo, "imperativo negativo"), (.presenteDeIndicativo, "presente de indicativo"), (.pretérito, "pretérito"), (.imperfectoDeIndicativo, "imperfecto de indicativo"), (.futuroDeIndicativo, "futuro de indicativo"), (.condicional, "condicional"), (.presenteDeSubjuntivo, "presente de subjuntivo"), (.imperfectoDeSubjuntivo1, "imperfecto de subjuntivo 1"), (.imperfectoDeSubjuntivo2, "imperfecto de subjuntivo 2"), (.futuroDeSubjuntivo, "futuro de subjuntivo"), (.perfectoDeIndicativo, "perfecto de indicativo"), (.pretéritoAnterior, "pretérito anterior"), (.pluscuamperfectoDeIndicativo, "pluscuamperfecto de indicativo"), (.futuroPerfecto, "futuro perfecto"), (.condicionalCompuesto, "condicional compuesto"), (.perfectoDeSubjuntivo, "perfecto de subjuntivo"), (.pluscuamperfectoDeSubjuntivo1, "pluscuamperfecto de subjuntivo 1"), (.pluscuamperfectoDeSubjuntivo2, "pluscuamperfecto de subjuntivo 2")].forEach {
15 | testDisplayName(tense: $0.0, displayName: $0.1)
16 | }
17 | }
18 |
19 | private func testDisplayName(tense: Tense, displayName: String) {
20 | XCTAssertEqual(tense.displayName, displayName)
21 | }
22 |
23 | func testTitleCaseNames() {
24 | [(Tense.infinitivo, "Infinitivo"), (.translation, "Translation"), (.gerundio, "Gerundio"), (.participio, "Participio"), (.raízFutura, "Raíz Futura"), (.imperativoPositivo, "Imperativo Positivo"), (.imperativoNegativo, "Imperativo Negativo"), (.presenteDeIndicativo, "Presente de Indicativo"), (.pretérito, "Pretérito"), (.imperfectoDeIndicativo, "Imperfecto de Indicativo"), (.futuroDeIndicativo, "Futuro de Indicativo"), (.condicional, "Condicional"), (.presenteDeSubjuntivo, "Presente de Subjuntivo"), (.imperfectoDeSubjuntivo1, "Imperfecto de Subjuntivo 1"), (.imperfectoDeSubjuntivo2, "Imperfecto de Subjuntivo 2"), (.futuroDeSubjuntivo, "Futuro de Subjuntivo"), (.perfectoDeIndicativo, "Perfecto de Indicativo"), (.pretéritoAnterior, "Pretérito Anterior"), (.pluscuamperfectoDeIndicativo, "Pluscuamperfecto de Indicativo"), (.futuroPerfecto, "Futuro Perfecto"), (.condicionalCompuesto, "Condicional Compuesto"), (.perfectoDeSubjuntivo, "Perfecto de Subjuntivo"), (.pluscuamperfectoDeSubjuntivo1, "Pluscuamperfecto de Subjuntivo 1"), (.pluscuamperfectoDeSubjuntivo2, "Pluscuamperfecto de Subjuntivo 2")].forEach {
25 | testTitleCaseName(tense: $0.0, titleCaseName: $0.1)
26 | }
27 | }
28 |
29 | private func testTitleCaseName(tense: Tense, titleCaseName: String) {
30 | XCTAssertEqual(tense.titleCaseName, titleCaseName)
31 | }
32 |
33 | func testHaberTensesForCompoundTenses() {
34 | // The following line uses as much type inference as the compiler allows.
35 | [(Tense.perfectoDeIndicativo, .presenteDeIndicativo), (.pretéritoAnterior, .pretérito), (.pluscuamperfectoDeIndicativo, .imperfectoDeIndicativo), (.futuroPerfecto, .futuroDeIndicativo), (.condicionalCompuesto, .condicional), (.perfectoDeSubjuntivo, .presenteDeSubjuntivo), (.pluscuamperfectoDeSubjuntivo1, .imperfectoDeSubjuntivo1), (.pluscuamperfectoDeSubjuntivo2, .imperfectoDeSubjuntivo1), (Tense.futuroPerfectoDeSubjuntivo, Tense.futuroDeSubjuntivo)].forEach {
36 | testHaberTenseForCompoundTense(compoundTense: $0.0, haberTense: $0.1)
37 | }
38 |
39 | let notACompoundTense: Tense = .infinitivo
40 | let result = notACompoundTense.haberTenseForCompoundTense()
41 | switch result {
42 | case .success(let haberTense):
43 | XCTFail("Haber form \(haberTense.displayName) incorrectly found for tense \(notACompoundTense.displayName).")
44 | case .failure:
45 | break
46 | }
47 | }
48 |
49 | private func testHaberTenseForCompoundTense(compoundTense: Tense, haberTense: Tense) {
50 | let result = compoundTense.haberTenseForCompoundTense()
51 | switch result {
52 | case .success(let haberTense):
53 | XCTAssertEqual(haberTense, haberTense)
54 | case .failure:
55 | XCTFail("No haber tense found for \(compoundTense.displayName).")
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Conjugar/BrowseInfoVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BrowseInfoVC.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 7/1/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class BrowseInfoVC: UIViewController, UITableViewDelegate, UITableViewDataSource, InfoDelegate {
12 | static let englishTitle = "Info"
13 |
14 | private var selectedRow = 0
15 | private var allInfos: [Info] = []
16 | private var easyModerateInfos: [Info] = []
17 | private var easyInfos: [Info] = []
18 |
19 | private var currentInfos: [Info] {
20 | switch browseInfoView.difficultyControl.selectedSegmentIndex {
21 | case 0:
22 | return easyInfos
23 | case 1:
24 | return easyModerateInfos
25 | case 2:
26 | return allInfos
27 | default:
28 | fatalError("Invalid UISegmentedControl index.")
29 | }
30 | }
31 |
32 | var browseInfoView: BrowseInfoUIV {
33 | if let castedView = view as? BrowseInfoUIV {
34 | return castedView
35 | } else {
36 | fatalError(fatalCastMessage(view: BrowseInfoUIV.self))
37 | }
38 | }
39 |
40 | override func loadView() {
41 | let browseInfoView: BrowseInfoUIV
42 | browseInfoView = BrowseInfoUIV(frame: UIScreen.main.bounds)
43 | browseInfoView.difficultyControl.addTarget(self, action: #selector(BrowseInfoVC.difficultyChanged(_:)), for: .valueChanged)
44 | browseInfoView.setupTable(dataSource: self, delegate: self)
45 | navigationItem.titleView = UILabel.titleLabel(title: Localizations.BrowseInfo.localizedTitle)
46 | easyInfos = Info.infos.filter {
47 | $0.difficulty == .easy
48 | }
49 | easyModerateInfos = Info.infos.filter {
50 | $0.difficulty == .easy || $0.difficulty == .moderate
51 | }
52 | allInfos = Info.infos
53 | view = browseInfoView
54 | }
55 |
56 | override func viewDidLoad() {
57 | super.viewDidLoad()
58 | updateDifficultyControl()
59 | }
60 |
61 | override func viewWillAppear(_ animated: Bool) {
62 | super.viewWillAppear(animated)
63 | Current.analytics.recordVisitation(viewController: "\(BrowseInfoVC.self)")
64 | }
65 |
66 | private func updateDifficultyControl() {
67 | switch Current.settings.infoDifficulty {
68 | case .easy:
69 | browseInfoView.difficultyControl.selectedSegmentIndex = 0
70 | case .moderate:
71 | browseInfoView.difficultyControl.selectedSegmentIndex = 1
72 | case .difficult:
73 | browseInfoView.difficultyControl.selectedSegmentIndex = 2
74 | }
75 | }
76 |
77 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
78 | return currentInfos.count
79 | }
80 |
81 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
82 | guard let cell = tableView.dequeueReusableCell(withIdentifier: InfoCell.identifier) as? InfoCell else {
83 | fatalError("Could not dequeue \(InfoCell.self).")
84 | }
85 | guard let decodedString = currentInfos[indexPath.row].heading.removingPercentEncoding else {
86 | fatalError("Could not decode string.")
87 | }
88 | cell.configure(heading: decodedString)
89 | return cell
90 | }
91 |
92 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
93 | selectedRow = (indexPath as NSIndexPath).row
94 | tableView.deselectRow(at: indexPath, animated: false)
95 | showInfo()
96 | }
97 |
98 | func infoSelectionDidChange(newHeading: String) {
99 | for i in 0 ..< Info.infos.count {
100 | if currentInfos[i].heading.lowercased() == newHeading.lowercased() {
101 | selectedRow = i
102 | break
103 | }
104 | }
105 | showInfo()
106 | }
107 |
108 | private func showInfo() {
109 | let infoVC = InfoVC(infoString: currentInfos[selectedRow].infoString, infoDelegate: self)
110 | navigationController?.pushViewController(infoVC, animated: true)
111 | }
112 |
113 | @objc func difficultyChanged(_ sender: UISegmentedControl) {
114 | let index = browseInfoView.difficultyControl.selectedSegmentIndex
115 | if index == 0 {
116 | Current.settings.infoDifficulty = .easy
117 | } else if index == 1 {
118 | Current.settings.infoDifficulty = .moderate
119 | } else /* index == 2 */ {
120 | Current.settings.infoDifficulty = .difficult
121 | }
122 | browseInfoView.table.reloadData()
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/Conjugar/World.swift:
--------------------------------------------------------------------------------
1 | //
2 | // World.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 1/15/19.
6 | // Enhanced by Stephen Celis on 1/16/19.
7 | // Copyright © 2019 Josh Adams. All rights reserved.
8 | //
9 |
10 | import Observation
11 | import SwiftUI
12 |
13 | #if targetEnvironment(simulator)
14 | var Current = World.simulator
15 | #else
16 | var Current = World.device
17 | #endif
18 |
19 | class World {
20 | var analytics: AnalyticsServiceable
21 | var reviewPrompter: ReviewPromptable
22 | var gameCenter: GameCenterable
23 | var settings: Settings
24 | var quiz: Quiz
25 | var session: URLSession
26 | var communGetter: CommunGetter
27 | var locale: Locale
28 | var parentViewController: UIViewController?
29 |
30 | private static let fakeRatingsCount = 42
31 |
32 | init(
33 | analytics: AnalyticsServiceable,
34 | reviewPrompter: ReviewPromptable,
35 | gameCenter: GameCenterable,
36 | settings: Settings,
37 | quiz: Quiz,
38 | session: URLSession,
39 | communGetter: CommunGetter,
40 | locale: Locale
41 | ) {
42 | self.analytics = analytics
43 | self.reviewPrompter = reviewPrompter
44 | self.gameCenter = gameCenter
45 | self.settings = settings
46 | self.quiz = quiz
47 | self.session = session
48 | self.communGetter = communGetter
49 | self.locale = locale
50 | }
51 |
52 | static let device: World = {
53 | let settings = Settings(getterSetter: UserDefaultsGetterSetter())
54 | let gameCenter = GameCenter.shared
55 |
56 | return World(
57 | analytics: AWSAnalyticsService(),
58 | reviewPrompter: ReviewPrompter(),
59 | gameCenter: gameCenter,
60 | settings: settings,
61 | quiz: Quiz(settings: settings, gameCenter: gameCenter, shouldShuffle: true),
62 | session: URLSession.shared,
63 | communGetter: CloudCommunGetter(),
64 | locale: RealLocale()
65 | )
66 | }()
67 |
68 | static let simulator: World = {
69 | let settings = Settings(getterSetter: UserDefaultsGetterSetter())
70 | let gameCenter = TestGameCenter()
71 |
72 | return World(
73 | analytics: TestAnalyticsService(),
74 | reviewPrompter: TestReviewPrompter(),
75 | gameCenter: gameCenter,
76 | settings: settings,
77 | quiz: Quiz(settings: settings, gameCenter: gameCenter, shouldShuffle: true),
78 | session: URLSession.stubSession(ratingsCount: fakeRatingsCount),
79 | communGetter: StubCommunGetter(),
80 | locale: StubLocale(languageCode: "en", regionCode: "US")
81 | )
82 | }()
83 |
84 | static let unitTest: World = {
85 | let settings = Settings(getterSetter: DictionaryGetterSetter())
86 | let gameCenter = TestGameCenter()
87 |
88 | return World(
89 | analytics: TestAnalyticsService(),
90 | reviewPrompter: TestReviewPrompter(),
91 | gameCenter: gameCenter,
92 | settings: settings,
93 | quiz: Quiz(settings: settings, gameCenter: gameCenter, shouldShuffle: false),
94 | session: URLSession.stubSession(ratingsCount: fakeRatingsCount),
95 | communGetter: StubCommunGetter(),
96 | locale: StubLocale()
97 | )
98 | }()
99 |
100 | static func uiTest(launchArguments arguments: [String]) -> World {
101 | let region: Region
102 | if arguments.contains(Region.spain.rawValue) {
103 | region = .spain
104 | } else if arguments.contains(Region.latinAmerica.rawValue) {
105 | region = .latinAmerica
106 | } else {
107 | region = Settings.regionDefault
108 | }
109 |
110 | let difficulty: Difficulty
111 | if arguments.contains(Difficulty.difficult.rawValue) {
112 | difficulty = .difficult
113 | } else if arguments.contains(Difficulty.moderate.rawValue) {
114 | difficulty = .moderate
115 | } else if arguments.contains(Difficulty.easy.rawValue) {
116 | difficulty = .easy
117 | } else {
118 | difficulty = Settings.difficultyDefault
119 | }
120 |
121 | let dictionary = [Settings.regionKey: region.rawValue, Settings.difficultyKey: difficulty.rawValue]
122 | let settings = Settings(getterSetter: DictionaryGetterSetter(dictionary: dictionary))
123 | let gameCenter = TestGameCenter()
124 |
125 | return World(
126 | analytics: TestAnalyticsService(),
127 | reviewPrompter: TestReviewPrompter(),
128 | gameCenter: gameCenter,
129 | settings: settings,
130 | quiz: Quiz(settings: settings, gameCenter: gameCenter, shouldShuffle: false),
131 | session: URLSession.stubSession(ratingsCount: fakeRatingsCount),
132 | communGetter: StubCommunGetter(),
133 | locale: StubLocale()
134 | )
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Conjugar.xcodeproj/xcshareddata/xcschemes/Conjugar.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
38 |
39 |
40 |
41 |
44 |
50 |
51 |
52 |
55 |
61 |
62 |
63 |
64 |
65 |
75 |
77 |
83 |
84 |
85 |
86 |
90 |
91 |
92 |
93 |
99 |
101 |
107 |
108 |
109 |
110 |
112 |
113 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/Conjugar/VerbVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VerbVC.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 4/10/17.
6 | // Copyright © 2017 Josh Adams. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class VerbVC: UIViewController {
12 | private let verb: String
13 | private var conjugationDataSource: ConjugationDataSource?
14 |
15 | var verbView: VerbUIV {
16 | if let castedView = view as? VerbUIV {
17 | return castedView
18 | } else {
19 | fatalError(fatalCastMessage(view: VerbUIV.self))
20 | }
21 | }
22 |
23 | init(verb: String) {
24 | self.verb = verb
25 | super.init(nibName: nil, bundle: nil)
26 | }
27 |
28 | required init?(coder aDecoder: NSCoder) {
29 | NSCoder.fatalErrorNotImplemented()
30 | }
31 |
32 | override func loadView() {
33 | let verbView = VerbUIV(frame: UIScreen.main.bounds)
34 | verbView.participio.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapSpanish(_:))))
35 | verbView.raízFutura.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapSpanish(_:))))
36 | verbView.gerundio.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapSpanish(_:))))
37 | verbView.translation.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapEnglish(_:))))
38 | verbView.defectuoso.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapEnglish(_:))))
39 | initNavigationItemTitleView()
40 | let translationResult = Conjugator.shared.conjugate(infinitive: verb, tense: .translation, personNumber: .none)
41 | switch translationResult {
42 | case let .success(value):
43 | verbView.translation.text = value
44 | default:
45 | fatalError()
46 | }
47 | let gerundioResult = Conjugator.shared.conjugate(infinitive: verb, tense: .gerundio, personNumber: .none)
48 | switch gerundioResult {
49 | case let .success(value):
50 | verbView.gerundio.attributedText = value.conjugatedString
51 | default:
52 | fatalError()
53 | }
54 | let participioResult = Conjugator.shared.conjugate(infinitive: verb, tense: .participio, personNumber: .none)
55 | switch participioResult {
56 | case let .success(value):
57 | verbView.participio.attributedText = value.conjugatedString
58 | default:
59 | fatalError()
60 | }
61 | let raízFuturaResult = Conjugator.shared.conjugate(infinitive: verb, tense: .raízFutura, personNumber: .none)
62 | switch raízFuturaResult {
63 | case let .success(value):
64 | verbView.raízFutura.attributedText = value.conjugatedString + NSAttributedString(string: "-")
65 | default:
66 | fatalError()
67 | }
68 | if Conjugator.shared.isDefective(infinitive: verb) {
69 | verbView.defectuoso.text = Localizations.Verb.defective
70 | } else {
71 | verbView.defectuoso.text = Localizations.Verb.notDefective
72 | }
73 |
74 | let verbType = Conjugator.shared.verbType(infinitive: verb)
75 | switch verbType {
76 | case .regularAr:
77 | verbView.parentOrType.text = "\(Localizations.Verb.regular) AR"
78 | case .regularEr:
79 | verbView.parentOrType.text = "\(Localizations.Verb.regular) ER"
80 | case .regularIr:
81 | verbView.parentOrType.text = "\(Localizations.Verb.regular) IR"
82 | case .irregular:
83 | guard let parent = Conjugator.shared.parent(infinitive: verb) else {
84 | fatalError("Parent verb not found.")
85 | }
86 | if Conjugator.baseVerbs.contains(parent) {
87 | verbView.parentOrType.text = Localizations.Verb.irregular
88 | } else {
89 | verbView.parentOrType.text = String(format: Localizations.Verb.irregularWithParent, parent)
90 | }
91 | }
92 | view = verbView
93 | }
94 |
95 | override func viewWillAppear(_ animated: Bool) {
96 | super.viewWillAppear(animated)
97 | conjugationDataSource = ConjugationDataSource(verb: verb, table: verbView.table, secondSingularBrowse: Current.settings.secondSingularBrowse)
98 | guard let conjugationDataSource = conjugationDataSource else {
99 | fatalError("\(ConjugationDataSource.self) was nil.")
100 | }
101 | verbView.setupTable(dataSource: conjugationDataSource, delegate: conjugationDataSource)
102 | verbView.table.reloadData()
103 | Current.analytics.recordVisitation(viewController: "\(VerbVC.self)")
104 | }
105 |
106 | private func initNavigationItemTitleView() {
107 | let titleLabel = UILabel.titleLabel(title: verb.capitalized)
108 | navigationItem.titleView = titleLabel
109 | titleLabel.isUserInteractionEnabled = true
110 | titleLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapSpanish(_:))))
111 | }
112 |
113 | @objc func tapSpanish(_ sender: UITapGestureRecognizer) {
114 | if let label = sender.view as? UILabel {
115 | Utterer.utter(label.attributedText?.string ?? label.text ?? "")
116 | }
117 | }
118 |
119 | @objc func tapEnglish(_ sender: UITapGestureRecognizer) {
120 | if let label = sender.view as? UILabel {
121 | Utterer.utter(label.attributedText?.string ?? label.text ?? "", locale: "en-US")
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/ConjugarUITests/QuizVCUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuizVCUITests.swift
3 | // ConjugarUITests
4 | //
5 | // Created by Joshua Adams on 12/8/18.
6 | // Copyright © 2018 Josh Adams. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class QuizVCUITests: XCTestCase {
12 | let enableUiTesting = "enable-ui-testing"
13 |
14 | override func setUp() {
15 | continueAfterFailure = false
16 | }
17 |
18 | override func tearDown() {}
19 |
20 | func testDifficultSpainQuiz() {
21 | testQuiz(answers: ["sintiendo", "midiendo", "caminando", "existiendo", "bebiendo", "andas", "ocurre", "leemos", "vais", "duermen", "hago", "fuiste", "dio", "pusimos", "trabajastéis", "recibieron", "aprendí", "ibas", "veía", "caminábamos", "andabais", "sabrán", "cabré", "trabajarás", "estudiará", "escucharíamos", "visitaríais", "podrían", "vaya", "hayas", "sepa", "estudiemos", "permitáis", "comprendan", "pudiera", "viajases", "estuviere", "enseñáremos", "ve", "llevad", "no llegen", "he cubierto", "hubiste bailado", "había dicho", "habremos nadado", "habríais escrito", "hayan cocinado", "hubiera hecho", "hubieses charlado", "hubiere muerto"], region: "Spain", difficulty: "Difficult")
22 | }
23 |
24 | func testDifficultLatinAmericaQuiz() {
25 | testQuiz(answers: ["sintiendo", "midiendo", "caminando", "existiendo", "bebiendo", "andas", "ocurre", "leemos", "van", "duermo", "haces", "fue", "dimos", "pusieron", "trabajé", "recibiste", "aprendió", "ibamos", "veían", "caminaba", "andabas", "sabrá", "cabremos", "trabajarán", "estudiaré", "escucharías", "visitaría", "podríamos", "vayan", "haya", "sepas", "estudie", "permitamos", "comprendan", "pudiera", "viajases", "estuviere", "enseñáremos", "ve", "lleven", "no llege", "hemos cubierto", "hubieron bailado", "había dicho", "habrás nadado", "habría escrito", "hayamos cocinado", "hubieran hecho", "hubiese charlado", "hubieres muerto"], region: "Latin America", difficulty: "Difficult")
26 | }
27 |
28 | func testModerateSpainQuiz() {
29 | testQuiz(answers: ["caminas", "anda", "existimos", "bebéis", "van", "duermo", "haces", "muere", "sabremos", "cabréis", "podrán", "caminaré", "andarás", "querría", "pondríamos", "tendríais", "trabajarían", "estudiaría", "has cubierto", "ha dicho", "hemos escrito", "habéis escuchado", "han visitado", "iba", "veías", "era", "viajábamos", "enseñabais", "llevaban", "fui", "diste", "puso", "trabajamos", "ocurristeis", "leieron", "vaya", "hayas", "sepa", "llegemos", "bailéis", "sintiendo", "midiendo", "nadando", "cocinando", "ve", "he", "charlen", "platice", "no lloremos", "no esperéis"], region: "Spain", difficulty: "Moderate")
30 | }
31 |
32 | func testModerateLatinAmericaQuiz() {
33 | testQuiz(answers: ["caminas", "anda", "existimos", "beben", "voy", "duermes", "hace", "morimos", "sabrán", "cabré", "podrás", "caminará", "andaremos", "querrían", "pondría", "tendrías", "trabajaría", "estudiaríamos", "han cubierto", "he dicho", "has escrito", "ha escuchado", "hemos visitado", "iban", "veía", "eras", "viajaba", "enseñábamos", "llevaban", "fui", "diste", "puso", "trabajamos", "ocurrieron", "leí", "vayas", "haya", "sepamos", "llegen", "baile", "sintiendo", "midiendo", "nadando", "cocinando", "ve", "he", "charle", "platicemos", "no lloren", "no espere"], region: "Latin America", difficulty: "Moderate")
34 | }
35 |
36 | func testEasySpainQuiz() {
37 | testQuiz(answers: ["caminas", "anda", "trabajamos", "existís", "ocurren", "recibo", "bebes", "lee", "aprendemos", "vais", "duermen", "hago", "mueres", "muerde", "oímos", "podéis", "han", "siento", "sabrás", "cabrá", "podremos", "querréis", "pondrán", "tendré", "vendrás", "saldrá", "estudiaremos", "escucharéis", "visitarán", "permitiré", "partirás", "comprenderá", "correremos", "fuisteis", "dieron", "puse", "pudiste", "estuvo", "tuvimos", "anduvisteis", "supieron", "caminé", "anduviste", "trabajó", "estudiamos", "escuchastéis", "visitaron", "viajé", "enseñaste", "llevó"], region: "Spain", difficulty: "Easy")
38 | }
39 |
40 | func testEasyLatinAmericaQuiz() {
41 | testQuiz(answers: ["caminas", "anda", "trabajamos", "existen", "ocurro", "recibes", "bebe", "leemos", "aprenden", "voy", "duermes", "hace", "morimos", "muerden", "oigo", "puedes", "ha", "sentimos", "sabrán", "cabré", "podrás", "querrá", "pondremos", "tendrán", "vendré", "saldrás", "estudiará", "escucharemos", "visitarán", "permitiré", "partirás", "comprenderá", "correremos", "fueron", "di", "pusiste", "pudo", "estuvimos", "tuvieron", "anduve", "supiste", "caminó", "anduvimos", "trabajaron", "estudié", "escuchaste", "visitó", "viajamos", "enseñaron", "llevé"], region: "Latin America", difficulty: "Easy")
42 | }
43 |
44 | func testQuiz(answers: [String], region: String, difficulty: String) {
45 | let app = XCUIApplication()
46 | app.launchArguments = [enableUiTesting, region, difficulty]
47 | app.launch()
48 | app.tabBars.buttons["Quiz"].tap()
49 | let timeout: TimeInterval = 1.0
50 | XCTAssert(app.buttons["Start"].waitForExistence(timeout: timeout))
51 | app.buttons["Start"].tap()
52 | let textField = app.textFields[" conjugation"]
53 | answers.forEach { conjugation in
54 | textField.typeText(conjugation + "\n")
55 | }
56 | XCTAssert(app.staticTexts["Results"].waitForExistence(timeout: timeout))
57 | XCTAssert(app.staticTexts[region].exists)
58 | XCTAssert(app.staticTexts[difficulty].exists)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Conjugar/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | AvenirNext-DemiBold
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/Conjugar/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "icon20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "icon20@2x-1.png",
13 | "language-direction" : "left-to-right",
14 | "scale" : "2x"
15 | },
16 | {
17 | "size" : "20x20",
18 | "idiom" : "iphone",
19 | "filename" : "icon20@2x-2.png",
20 | "language-direction" : "right-to-left",
21 | "scale" : "2x"
22 | },
23 | {
24 | "size" : "20x20",
25 | "idiom" : "iphone",
26 | "filename" : "icon20@3x.png",
27 | "scale" : "3x"
28 | },
29 | {
30 | "size" : "20x20",
31 | "idiom" : "iphone",
32 | "filename" : "icon20@3x-2.png",
33 | "language-direction" : "left-to-right",
34 | "scale" : "3x"
35 | },
36 | {
37 | "size" : "20x20",
38 | "idiom" : "iphone",
39 | "filename" : "icon20@3x-1.png",
40 | "language-direction" : "right-to-left",
41 | "scale" : "3x"
42 | },
43 | {
44 | "size" : "29x29",
45 | "idiom" : "iphone",
46 | "filename" : "icon29.png",
47 | "language-direction" : "left-to-right",
48 | "scale" : "1x"
49 | },
50 | {
51 | "size" : "29x29",
52 | "idiom" : "iphone",
53 | "filename" : "icon29-1.png",
54 | "language-direction" : "right-to-left",
55 | "scale" : "1x"
56 | },
57 | {
58 | "size" : "29x29",
59 | "idiom" : "iphone",
60 | "filename" : "icon29@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "29x29",
65 | "idiom" : "iphone",
66 | "filename" : "icon29@2x-1.png",
67 | "language-direction" : "left-to-right",
68 | "scale" : "2x"
69 | },
70 | {
71 | "size" : "29x29",
72 | "idiom" : "iphone",
73 | "filename" : "icon29@2x-2.png",
74 | "language-direction" : "right-to-left",
75 | "scale" : "2x"
76 | },
77 | {
78 | "size" : "29x29",
79 | "idiom" : "iphone",
80 | "filename" : "icon29@3x.png",
81 | "scale" : "3x"
82 | },
83 | {
84 | "size" : "29x29",
85 | "idiom" : "iphone",
86 | "filename" : "icon29@3x-1.png",
87 | "language-direction" : "left-to-right",
88 | "scale" : "3x"
89 | },
90 | {
91 | "size" : "29x29",
92 | "idiom" : "iphone",
93 | "filename" : "icon29@3x-2.png",
94 | "language-direction" : "right-to-left",
95 | "scale" : "3x"
96 | },
97 | {
98 | "size" : "40x40",
99 | "idiom" : "iphone",
100 | "filename" : "icon40@2x.png",
101 | "scale" : "2x"
102 | },
103 | {
104 | "size" : "40x40",
105 | "idiom" : "iphone",
106 | "filename" : "icon40@2x-1.png",
107 | "language-direction" : "left-to-right",
108 | "scale" : "2x"
109 | },
110 | {
111 | "size" : "40x40",
112 | "idiom" : "iphone",
113 | "filename" : "icon40@2x-2.png",
114 | "language-direction" : "right-to-left",
115 | "scale" : "2x"
116 | },
117 | {
118 | "size" : "40x40",
119 | "idiom" : "iphone",
120 | "filename" : "icon40@3x.png",
121 | "scale" : "3x"
122 | },
123 | {
124 | "size" : "40x40",
125 | "idiom" : "iphone",
126 | "filename" : "icon40@3x-1.png",
127 | "language-direction" : "left-to-right",
128 | "scale" : "3x"
129 | },
130 | {
131 | "size" : "40x40",
132 | "idiom" : "iphone",
133 | "filename" : "icon40@3x-2.png",
134 | "language-direction" : "right-to-left",
135 | "scale" : "3x"
136 | },
137 | {
138 | "size" : "57x57",
139 | "idiom" : "iphone",
140 | "language-direction" : "left-to-right",
141 | "scale" : "1x"
142 | },
143 | {
144 | "size" : "57x57",
145 | "idiom" : "iphone",
146 | "language-direction" : "right-to-left",
147 | "scale" : "1x"
148 | },
149 | {
150 | "size" : "57x57",
151 | "idiom" : "iphone",
152 | "language-direction" : "left-to-right",
153 | "scale" : "2x"
154 | },
155 | {
156 | "size" : "57x57",
157 | "idiom" : "iphone",
158 | "language-direction" : "right-to-left",
159 | "scale" : "2x"
160 | },
161 | {
162 | "size" : "60x60",
163 | "idiom" : "iphone",
164 | "filename" : "icon60@2x.png",
165 | "scale" : "2x"
166 | },
167 | {
168 | "size" : "60x60",
169 | "idiom" : "iphone",
170 | "filename" : "icon60@2x-1.png",
171 | "language-direction" : "left-to-right",
172 | "scale" : "2x"
173 | },
174 | {
175 | "size" : "60x60",
176 | "idiom" : "iphone",
177 | "filename" : "icon60@2x-2.png",
178 | "language-direction" : "right-to-left",
179 | "scale" : "2x"
180 | },
181 | {
182 | "size" : "60x60",
183 | "idiom" : "iphone",
184 | "filename" : "icon60@3x.png",
185 | "scale" : "3x"
186 | },
187 | {
188 | "size" : "60x60",
189 | "idiom" : "iphone",
190 | "filename" : "icon60@3x-1.png",
191 | "language-direction" : "left-to-right",
192 | "scale" : "3x"
193 | },
194 | {
195 | "size" : "60x60",
196 | "idiom" : "iphone",
197 | "filename" : "icon60@3x-2.png",
198 | "language-direction" : "right-to-left",
199 | "scale" : "3x"
200 | },
201 | {
202 | "size" : "1024x1024",
203 | "idiom" : "ios-marketing",
204 | "filename" : "icon1024.png",
205 | "scale" : "1x"
206 | }
207 | ],
208 | "info" : {
209 | "version" : 1,
210 | "author" : "xcode"
211 | }
212 | }
--------------------------------------------------------------------------------
/Conjugar/CloudCommunGetter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CloudCommunGetter.swift
3 | // Conjugar
4 | //
5 | // Created by Joshua Adams on 12/18/20.
6 | // Copyright © 2020 Josh Adams. All rights reserved.
7 | //
8 |
9 | import CloudKit
10 | import MessageUI
11 | import UIKit
12 |
13 | struct CloudCommunGetter: CommunGetter {
14 | func getCommunication(completion: @escaping (Commun) -> Void) {
15 | let predicate = NSPredicate(format: "isCurrent == 1")
16 | let query = CKQuery(recordType: "Communs", predicate: predicate)
17 | let identifier = "iCloud.biz.Conjugar"
18 |
19 | // TODO: Perhaps use something like this to fix deprecation in code after this.
20 | // TODO: When done, reenable getCommunication() in MainTabBarVC.
21 | // CKContainer(identifier: identifier).publicCloudDatabase.fetch(withQuery: query) { result in
22 | // switch result {
23 | // case .success((let matchResults, let queryCursor)):
24 | // <#code#>
25 | // case .failure:
26 | // return
27 | // }
28 | // }
29 |
30 | CKContainer(identifier: identifier).publicCloudDatabase.perform(query, inZoneWith: nil) { records, error in
31 | guard
32 | error == nil,
33 | let records = records,
34 | let record = records.first
35 | else {
36 | return
37 | }
38 |
39 | let separator = "|"
40 |
41 | guard
42 | let titleString = record["title"] as? String,
43 | let titleDict = dictFromString(string: titleString, primarySeparator: separator),
44 | let contentString = record["content"] as? String,
45 | let contentDict = dictFromString(string: contentString, primarySeparator: separator),
46 | let okayTitleString = record["okayTitle"] as? String,
47 | let okayTitleDict = dictFromString(string: okayTitleString, primarySeparator: separator),
48 | let cancelTitleString = record["cancelTitle"] as? String,
49 | let cancelTitleDict = dictFromString(string: cancelTitleString, primarySeparator: separator),
50 | let actionTitleString = record["actionTitle"] as? String,
51 | let actionTitleDict = dictFromString(string: actionTitleString, primarySeparator: separator),
52 | let typeString = record["type"] as? String,
53 | let cloudSchemaVersion = record["version"] as? Int,
54 | let imageAsset = record["image"] as? CKAsset,
55 | let imageFileUrl = imageAsset.fileURL,
56 | let imageData = try? Data(contentsOf: imageFileUrl),
57 | let image = UIImage(data: imageData),
58 | let imageLabelString = record["imageLabel"] as? String,
59 | let imageLabelDict = dictFromString(string: imageLabelString, primarySeparator: separator),
60 | let identifier = record["identifier"] as? Int
61 | else {
62 | return
63 | }
64 |
65 | let typeElements = typeString.components(separatedBy: separator)
66 | let typeFirstElement = "\(typeElements[0])"
67 |
68 | var type: Commun.CommunType?
69 | switch typeFirstElement {
70 | case "newVersion":
71 | guard
72 | typeElements.count > 1,
73 | let cloudVersion = Double(typeElements[1]),
74 | let appVersionString = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
75 | let appVersion = Double(appVersionString),
76 | let openUrlClosure = openUrlClosure(urlString: "https://itunes.apple.com/\(Current.locale.regionCode)/app/conjugar/id1236500467")
77 | else {
78 | return
79 | }
80 |
81 | let alreadyUpdated = appVersion >= cloudVersion
82 |
83 | type = Commun.CommunType.newVersion(
84 | okayTitle: okayTitleDict,
85 | actionTitle: actionTitleDict,
86 | cancelTitle: cancelTitleDict,
87 | action: openUrlClosure,
88 | alreadyUpdated: alreadyUpdated
89 | )
90 |
91 | case "website":
92 | guard
93 | typeElements.count > 1,
94 | let openUrlClosure = openUrlClosure(urlString: typeElements[1])
95 | else {
96 | return
97 | }
98 |
99 | type = Commun.CommunType.website(actionTitle: actionTitleDict, cancelTitle: cancelTitleDict, action: openUrlClosure)
100 |
101 | case "information":
102 | type = Commun.CommunType.information(okayTitle: okayTitleDict)
103 |
104 | case "email":
105 | if let sendEmailClosure = Emailer.shared.sendEmailClosure {
106 | type = Commun.CommunType.email(actionTitle: actionTitleDict, cancelTitle: cancelTitleDict, action: sendEmailClosure)
107 | }
108 |
109 | default:
110 | break
111 | }
112 |
113 | let appSchemaVersion = 0
114 | if
115 | let type = type,
116 | appSchemaVersion >= cloudSchemaVersion
117 | {
118 | let commun = Commun(title: titleDict, image: image, imageLabel: imageLabelDict, content: contentDict, type: type, identifier: identifier)
119 | completion(commun)
120 | }
121 | }
122 | }
123 |
124 | private func dictFromString(string: String, primarySeparator: String) -> [String: String]? {
125 | guard !string.isEmpty else {
126 | return nil
127 | }
128 | let variants = string.components(separatedBy: primarySeparator)
129 | guard !variants.isEmpty else {
130 | return nil
131 | }
132 | var dict: [String: String] = [:]
133 | let secondarySeparator = "="
134 | for variant in variants {
135 | let components = variant.components(separatedBy: secondarySeparator)
136 | guard
137 | components.count == 2,
138 | ["en", "es"].contains(components[0])
139 | else {
140 | continue
141 | }
142 | dict[components[0]] = components[1]
143 | }
144 |
145 | if !dict.isEmpty {
146 | return dict
147 | } else {
148 | return nil
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------